Browse Source

Introduce Windows support for the client stack

pull/1652/head
Kévin Chalet 3 years ago
parent
commit
532a5941ec
  1. 4
      Directory.Build.props
  2. 16
      Directory.Build.targets
  3. 16
      Directory.Packages.props
  4. 28
      OpenIddict.sln
  5. 102
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  6. 82
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  7. 24
      sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj
  8. 99
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  9. 54
      sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs
  10. 73
      sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs
  11. 105
      sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs
  12. 31
      sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj
  13. 94
      sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
  14. 55
      sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs
  15. 7
      sandbox/OpenIddict.Sandbox.Wpf.Client/App.xaml
  16. 8
      sandbox/OpenIddict.Sandbox.Wpf.Client/App.xaml.cs
  17. 12
      sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml
  18. 66
      sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs
  19. 32
      sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj
  20. 99
      sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs
  21. 55
      sandbox/OpenIddict.Sandbox.Wpf.Client/Worker.cs
  22. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  23. 43
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  24. 17
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  25. 36
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  26. 17
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  27. 36
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  28. 18
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  29. 41
      src/OpenIddict.Client.Windows/OpenIddict.Client.Windows.csproj
  30. 29
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs
  31. 131
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs
  32. 108
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs
  33. 25
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsConstants.cs
  34. 95
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs
  35. 68
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs
  36. 82
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.Authentication.cs
  37. 1320
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs
  38. 21
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs
  39. 161
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs
  40. 130
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsMarshaller.cs
  41. 50
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs
  42. 103
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs
  43. 20
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  44. 127
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  45. 246
      src/OpenIddict.Client/OpenIddictClientService.cs
  46. 11
      src/OpenIddict.Client/OpenIddictClientTransaction.cs
  47. 18
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  48. 19
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  49. 15
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  50. 11
      src/OpenIddict.Server/OpenIddictServerTransaction.cs
  51. 15
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs
  52. 15
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  53. 15
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  54. 11
      src/OpenIddict.Validation/OpenIddictValidationTransaction.cs
  55. 8
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTestServer.cs
  56. 8
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs
  57. 8
      test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTestServer.cs
  58. 8
      test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTests.cs

4
Directory.Build.props

@ -66,6 +66,10 @@
<Serviceable>false</Serviceable>
</PropertyGroup>
<PropertyGroup>
<PolySharpIncludeRuntimeSupportedAttributes>true</PolySharpIncludeRuntimeSupportedAttributes>
</PropertyGroup>
<ItemGroup>
<ProjectCapability Include="DynamicDependentFile" />
<ProjectCapability Include="DynamicFileNesting" />

16
Directory.Build.targets

@ -29,7 +29,12 @@
</PropertyGroup>
<PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp') Or
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '3.5'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL</DefineConstants>
</PropertyGroup>
<PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '1.0'))) Or
('$(TargetFrameworkIdentifier)' == '.NETFramework' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '4.7'))) Or
('$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '1.6'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_ECDSA</DefineConstants>
@ -57,12 +62,14 @@
<PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.2'))) Or
('$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.1'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_GENERIC_HOST</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER</DefineConstants>
</PropertyGroup>
<PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '3.0'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_HOST_APPLICATION_LIFETIME</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_HOST_ENVIRONMENT</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_INTEGER32_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants>
</PropertyGroup>
@ -75,10 +82,13 @@
<PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '5.0'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ENVIRONMENT_PROCESS_PATH</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION_POLICY</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ONE_SHOT_HASHING_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_PEM_ENCODED_KEY_IMPORT</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_WINFORMS_TASK_DIALOG</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL</DefineConstants>
</PropertyGroup>
<PropertyGroup

16
Directory.Packages.props

@ -29,6 +29,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.23" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="2.1.23" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="2.1.1" />
@ -182,12 +183,17 @@
<PackageVersion Include="Autofac.Mvc5" Version="6.0.0" />
<PackageVersion Include="Autofac.Owin" Version="6.0.1" />
<PackageVersion Include="Autofac.WebApi2.Owin" Version="6.0.0" />
<PackageVersion Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" Version="1.0.4" />
<PackageVersion Include="Dapplo.Microsoft.Extensions.Hosting.WinForms" Version="1.0.4" />
<PackageVersion Include="Dapplo.Microsoft.Extensions.Hosting.Wpf" Version="1.0.4" />
<PackageVersion Include="Microsoft.AspNet.Identity.EntityFramework" Version="2.2.3" />
<PackageVersion Include="Microsoft.AspNet.Identity.Owin" Version="2.2.3" />
<PackageVersion Include="Microsoft.AspNet.Mvc" Version="5.2.9" />
<PackageVersion Include="Microsoft.AspNet.Web.Optimization" Version="1.1.3" />
<PackageVersion Include="Microsoft.AspNet.WebApi.Owin" Version="5.2.9" />
<PackageVersion Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" Version="3.6.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.14" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="3.8.0" />
<PackageVersion Include="Microsoft.Owin.Host.SystemWeb" Version="4.2.2" />
<PackageVersion Include="Microsoft.Owin.Security.Cookies" Version="4.2.2" />
@ -219,6 +225,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="3.1.32" />
@ -228,6 +235,7 @@
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="NamedPipeServerStream.NetFrameworkVersion" Version="1.1.7" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.5.0" />
<PackageVersion Include="SmartFormat" Version="3.2.0" />
<PackageVersion Include="System.Net.Http.Json" Version="3.2.1" />
@ -264,6 +272,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="6.0.12" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0" />
@ -308,6 +317,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.0" />
@ -333,8 +343,14 @@
<!--
Note: the following references are exclusively used in the samples:
-->
<PackageVersion Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" Version="1.0.4" />
<PackageVersion Include="Dapplo.Microsoft.Extensions.Hosting.WinForms" Version="1.0.4" />
<PackageVersion Include="Dapplo.Microsoft.Extensions.Hosting.Wpf" Version="1.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.5.0" />
<!--

28
OpenIddict.sln

@ -147,6 +147,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.Owin.Inte
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.DataProtection.Tests", "test\OpenIddict.Server.DataProtection.Tests\OpenIddict.Server.DataProtection.Tests.csproj", "{C92838AB-3923-49A1-B23E-FA01306CAC9D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.Windows", "src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj", "{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Wpf.Client", "sandbox\OpenIddict.Sandbox.Wpf.Client\OpenIddict.Sandbox.Wpf.Client.csproj", "{D0C56832-6557-4CC1-91EA-7B31B18BEE7A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.WinForms.Client", "sandbox\OpenIddict.Sandbox.WinForms.Client\OpenIddict.Sandbox.WinForms.Client.csproj", "{35997586-8AAB-4EE5-A022-3E5BD12746CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Console.Client", "sandbox\OpenIddict.Sandbox.Console.Client\OpenIddict.Sandbox.Console.Client.csproj", "{819CD7AA-01FD-4369-BABF-5DFCB7E94068}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -357,6 +365,22 @@ Global
{C92838AB-3923-49A1-B23E-FA01306CAC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C92838AB-3923-49A1-B23E-FA01306CAC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C92838AB-3923-49A1-B23E-FA01306CAC9D}.Release|Any CPU.Build.0 = Release|Any CPU
{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}.Debug|Any CPU.Build.0 = Debug|Any CPU
{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}.Release|Any CPU.ActiveCfg = Release|Any CPU
{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}.Release|Any CPU.Build.0 = Release|Any CPU
{D0C56832-6557-4CC1-91EA-7B31B18BEE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0C56832-6557-4CC1-91EA-7B31B18BEE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0C56832-6557-4CC1-91EA-7B31B18BEE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0C56832-6557-4CC1-91EA-7B31B18BEE7A}.Release|Any CPU.Build.0 = Release|Any CPU
{35997586-8AAB-4EE5-A022-3E5BD12746CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35997586-8AAB-4EE5-A022-3E5BD12746CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35997586-8AAB-4EE5-A022-3E5BD12746CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35997586-8AAB-4EE5-A022-3E5BD12746CE}.Release|Any CPU.Build.0 = Release|Any CPU
{819CD7AA-01FD-4369-BABF-5DFCB7E94068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{819CD7AA-01FD-4369-BABF-5DFCB7E94068}.Debug|Any CPU.Build.0 = Debug|Any CPU
{819CD7AA-01FD-4369-BABF-5DFCB7E94068}.Release|Any CPU.ActiveCfg = Release|Any CPU
{819CD7AA-01FD-4369-BABF-5DFCB7E94068}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -413,6 +437,10 @@ Global
{CC731B63-4D5C-4587-8F28-B40F4EEAC735} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{2F3E9EED-446B-46C3-BC52-ED66C280E0A3} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{C92838AB-3923-49A1-B23E-FA01306CAC9D} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{058BF159-F3DB-4BCD-99E5-2E5C62D8F588} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{D0C56832-6557-4CC1-91EA-7B31B18BEE7A} = {F47D1283-0EE9-4728-8026-58405C29B786}
{35997586-8AAB-4EE5-A022-3E5BD12746CE} = {F47D1283-0EE9-4728-8026-58405C29B786}
{819CD7AA-01FD-4369-BABF-5DFCB7E94068} = {F47D1283-0EE9-4728-8026-58405C29B786}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

102
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -26,6 +26,40 @@ public class Worker : IHostedService
{
var manager = provider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("console") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "console",
ConsentType = ConsentTypes.Systematic,
DisplayName = "Console client application",
DisplayNames =
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente console"
},
RedirectUris =
{
new Uri("openiddict-sandbox-console-client://localhost/callback/login/local")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
if (await manager.FindByClientIdAsync("mvc") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
@ -66,6 +100,74 @@ public class Worker : IHostedService
});
}
if (await manager.FindByClientIdAsync("winforms") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "winforms",
ConsentType = ConsentTypes.Systematic,
DisplayName = "WinForms client application",
DisplayNames =
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente WinForms"
},
RedirectUris =
{
new Uri("openiddict-sandbox-winforms-client://localhost/callback/login/local")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
if (await manager.FindByClientIdAsync("wpf") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "wpf",
ConsentType = ConsentTypes.Systematic,
DisplayName = "WPF client application",
DisplayNames =
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente WPF"
},
RedirectUris =
{
new Uri("openiddict-sandbox-wpf-client://localhost/callback/login/local")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
// Note: when using introspection instead of local token validation,
// an application entry MUST be created to allow the resource server
// to communicate with OpenIddict's introspection endpoint.

82
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -0,0 +1,82 @@
using Microsoft.Extensions.Hosting;
using OpenIddict.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Abstractions.OpenIddictExceptions;
using static System.Console;
namespace OpenIddict.Sandbox.Console.Client;
public class InteractiveService : BackgroundService
{
private readonly IHostApplicationLifetime _lifetime;
private readonly OpenIddictClientService _service;
public InteractiveService(
IHostApplicationLifetime lifetime,
OpenIddictClientService service)
{
_lifetime = lifetime;
_service = service;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Wait for the host to confirm that the application has started.
var source = new TaskCompletionSource();
using (_lifetime.ApplicationStarted.Register(static state => ((TaskCompletionSource) state!).SetResult(), source))
{
await source.Task;
}
string? provider;
while (!stoppingToken.IsCancellationRequested)
{
do
{
await Out.WriteLineAsync("Type '1' + ENTER to log in using the local server or '2' + ENTER to log in using Twitter");
provider = await In.ReadLineAsync(stoppingToken) switch
{
"1" => "Local",
"2" => "Twitter",
_ => null
};
}
while (string.IsNullOrEmpty(provider));
await Out.WriteLineAsync("Launching the system browser.");
try
{
// Ask OpenIddict to initiate the challenge and launch the system browser
// to allow the user to complete the interactive authentication dance.
var nonce = await _service.ChallengeWithBrowserAsync(
provider, cancellationToken: stoppingToken);
// Wait until the user approved or rejected the authorization
// demand and retrieve the resulting claims-based principal.
var (_, _, principal) = await _service.AuthenticateWithBrowserAsync(
nonce, cancellationToken: stoppingToken);
await Out.WriteLineAsync($"Welcome, {principal.FindFirst(Claims.Name)!.Value}.");
}
catch (OperationCanceledException)
{
await Error.WriteLineAsync("The authentication process was aborted.");
}
catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied)
{
await Error.WriteLineAsync("The authorization was denied by the end user.");
}
catch
{
await Error.WriteLineAsync("An error occurred while trying to authenticate the user.");
}
}
}
}

24
sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<IsShipping>false</IsShipping>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
</Project>

99
sandbox/OpenIddict.Sandbox.Console.Client/Program.cs

@ -0,0 +1,99 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenIddict.Client;
using OpenIddict.Sandbox.Console.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
var host = Host.CreateDefaultBuilder(args)
// Note: applications for which a single instance is preferred can reference
// the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this
// method to automatically close extra instances based on the specified identifier:
//
// .ConfigureSingleInstance(options => options.MutexId = "{802A478D-00E8-4DAE-9A27-27B31A47CB39}")
//
.ConfigureServices(services =>
{
services.AddDbContext<DbContext>(options =>
{
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-console-client.sqlite3")}");
options.UseOpenIddict();
});
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict client components.
.AddClient(options =>
{
// Note: this sample uses the authorization code and refresh token
// flows, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the Windows host.
options.UseWindows();
// Set the client URI that will uniquely identify this application.
options.SetClientUri(new Uri("openiddict-sandbox-console-client://localhost/", UriKind.Absolute));
// Register the System.Net.Http integration and use the identity of the current
// assembly as a more specific user agent, which can be useful when dealing with
// providers that use the user agent as a way to throttle requests (e.g Reddit).
options.UseSystemNetHttp()
.SetProductInformation(typeof(Program).Assembly);
// Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration
{
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
ProviderName = "Local",
ClientId = "console",
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
});
// Register the Web providers integrations.
//
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative));
});
// Register the worker responsible for creating the database used to store tokens
// and adding the registry entries required to register the custom URI scheme.
//
// Note: in a real world application, this step should be part of a setup script.
services.AddHostedService<Worker>();
// Register the background service responsible for handling the console interactions.
services.AddHostedService<InteractiveService>();
services.RemoveAll<ILoggerProvider>();
})
.UseConsoleLifetime()
.Build();
await host.RunAsync();

54
sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs

@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Win32;
namespace OpenIddict.Sandbox.Console.Client;
public class Worker : IHostedService
{
private readonly IServiceProvider _provider;
public Worker(IServiceProvider provider)
=> _provider = provider;
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
RegistryKey? root = null;
// Create the registry entries necessary to handle URI protocol activations.
// Note: the application MUST be run once as an administrator for this to work.
try
{
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-console-client");
if (root is null)
{
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-console-client");
root.SetValue(string.Empty, "URL:openiddict-sandbox-console-client");
root.SetValue("URL Protocol", string.Empty);
using var command = root.CreateSubKey("shell\\open\\command");
command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"",
#if SUPPORTS_ENVIRONMENT_PROCESS_PATH
Environment.ProcessPath
#else
Process.GetCurrentProcess().MainModule.FileName
#endif
));
}
}
finally
{
root?.Dispose();
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

73
sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.Designer.cs

@ -0,0 +1,73 @@
namespace OpenIddict.Sandbox.WinForms.Client
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.LocalLogin = new System.Windows.Forms.Button();
this.TwitterLogin = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// LocalLogin
//
this.LocalLogin.Location = new System.Drawing.Point(258, 93);
this.LocalLogin.Name = "LocalLogin";
this.LocalLogin.Size = new System.Drawing.Size(283, 83);
this.LocalLogin.TabIndex = 0;
this.LocalLogin.Text = "Log in using the local server";
this.LocalLogin.UseVisualStyleBackColor = true;
this.LocalLogin.Click += new System.EventHandler(this.LocalLoginButton_Click);
//
// TwitterLogin
//
this.TwitterLogin.Location = new System.Drawing.Point(258, 258);
this.TwitterLogin.Name = "TwitterLogin";
this.TwitterLogin.Size = new System.Drawing.Size(283, 83);
this.TwitterLogin.TabIndex = 1;
this.TwitterLogin.Text = "Log in using Twitter";
this.TwitterLogin.UseVisualStyleBackColor = true;
this.TwitterLogin.Click += new System.EventHandler(this.TwitterLoginButton_Click);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.TwitterLogin);
this.Controls.Add(this.LocalLogin);
this.Name = "MainForm";
this.Text = "OpenIddict WinForms client";
this.ResumeLayout(false);
}
#endregion
private Button LocalLogin;
private Button TwitterLogin;
}
}

105
sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs

@ -0,0 +1,105 @@
using Dapplo.Microsoft.Extensions.Hosting.WinForms;
using OpenIddict.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Abstractions.OpenIddictExceptions;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Sandbox.WinForms.Client
{
public partial class MainForm : Form, IWinFormsShell
{
private readonly OpenIddictClientService _service;
public MainForm(OpenIddictClientService service)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
InitializeComponent();
}
private async void LocalLoginButton_Click(object sender, EventArgs e)
=> await AuthenticateAsync("Local");
private async void TwitterLoginButton_Click(object sender, EventArgs e)
=> await AuthenticateAsync(Providers.Twitter);
private async Task AuthenticateAsync(string provider)
{
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90));
try
{
// Ask OpenIddict to initiate the challenge and launch the system browser
// to allow the user to complete the interactive authentication dance.
var nonce = await _service.ChallengeWithBrowserAsync(
provider, cancellationToken: source.Token);
// Wait until the user approved or rejected the authorization
// demand and retrieve the resulting claims-based principal.
var (_, _, principal) = await _service.AuthenticateWithBrowserAsync(
nonce, cancellationToken: source.Token);
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Authentication successful",
Heading = "Authentication successful",
Icon = TaskDialogIcon.ShieldSuccessGreenBar,
Text = $"Welcome, {principal.FindFirst(Claims.Name)!.Value}."
});
#else
MessageBox.Show($"Welcome, {principal.FindFirst(Claims.Name)!.Value}.",
"Authentication successful", MessageBoxButtons.OK, MessageBoxIcon.Information);
#endif
}
catch (OperationCanceledException)
{
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Authentication timed out",
Heading = "Authentication timed out",
Icon = TaskDialogIcon.Warning,
Text = "The authentication process was aborted."
});
#else
MessageBox.Show("The authentication process was aborted.",
"Authentication timed out", MessageBoxButtons.OK, MessageBoxIcon.Warning);
#endif
}
catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied)
{
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Authorization denied",
Heading = "Authorization denied",
Icon = TaskDialogIcon.Warning,
Text = "The authorization was denied by the end user."
});
#else
MessageBox.Show("The authorization was denied by the end user.",
"Authorization denied", MessageBoxButtons.OK, MessageBoxIcon.Warning);
#endif
}
catch
{
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage
{
Caption = "Authentication failed",
Heading = "Authentication failed",
Icon = TaskDialogIcon.Error,
Text = "An error occurred while trying to authenticate the user."
});
#else
MessageBox.Show("An error occurred while trying to authenticate the user.",
"Authentication failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
#endif
}
}
}
}

31
sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net48;net7.0-windows</TargetFrameworks>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UseWindowsForms>true</UseWindowsForms>
<IsShipping>false</IsShipping>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" />
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.WinForms" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<Using Remove="System.Net.Http" />
</ItemGroup>
</Project>

94
sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs

@ -0,0 +1,94 @@
using Dapplo.Microsoft.Extensions.Hosting.WinForms;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Client;
using OpenIddict.Sandbox.WinForms.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
var host = Host.CreateDefaultBuilder(args)
// Note: applications for which a single instance is preferred can reference
// the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this
// method to automatically close extra instances based on the specified identifier:
//
// .ConfigureSingleInstance(options => options.MutexId = "{D6FEAFC8-3079-4881-B9F2-0B78EAF38B85}")
//
.ConfigureServices(services =>
{
services.AddDbContext<DbContext>(options =>
{
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-winforms-client.sqlite3")}");
options.UseOpenIddict();
});
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict client components.
.AddClient(options =>
{
// Note: this sample uses the authorization code and refresh token
// flows, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the Windows host.
options.UseWindows();
// Set the client URI that will uniquely identify this application.
options.SetClientUri(new Uri("openiddict-sandbox-winforms-client://localhost/", UriKind.Absolute));
// Register the System.Net.Http integration and use the identity of the current
// assembly as a more specific user agent, which can be useful when dealing with
// providers that use the user agent as a way to throttle requests (e.g Reddit).
options.UseSystemNetHttp()
.SetProductInformation(typeof(Program).Assembly);
// Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration
{
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
ProviderName = "Local",
ClientId = "winforms",
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
});
// Register the Web providers integrations.
//
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative));
});
// Register the worker responsible for creating the database used to store tokens
// and adding the registry entries required to register the custom URI scheme.
//
// Note: in a real world application, this step should be part of a setup script.
services.AddHostedService<Worker>();
})
.ConfigureWinForms<MainForm>()
.UseWinFormsLifetime()
.Build();
await host.RunAsync();

55
sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs

@ -0,0 +1,55 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Win32;
namespace OpenIddict.Sandbox.WinForms.Client;
public class Worker : IHostedService
{
private readonly IServiceProvider _provider;
public Worker(IServiceProvider provider)
=> _provider = provider;
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
RegistryKey? root = null;
// Create the registry entries necessary to handle URI protocol activations.
// Note: the application MUST be run once as an administrator for this to work.
try
{
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-winforms-client");
if (root is null)
{
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-winforms-client");
root.SetValue(string.Empty, "URL:openiddict-sandbox-winforms-client");
root.SetValue("URL Protocol", string.Empty);
using var command = root.CreateSubKey("shell\\open\\command");
command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"",
#if SUPPORTS_ENVIRONMENT_PROCESS_PATH
Environment.ProcessPath
#else
Process.GetCurrentProcess().MainModule.FileName
#endif
));
}
}
finally
{
root?.Dispose();
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

7
sandbox/OpenIddict.Sandbox.Wpf.Client/App.xaml

@ -0,0 +1,7 @@
<Application x:Class="OpenIddict.Sandbox.Wpf.Client.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>

8
sandbox/OpenIddict.Sandbox.Wpf.Client/App.xaml.cs

@ -0,0 +1,8 @@
using System.Windows;
namespace OpenIddict.Sandbox.Wpf.Client
{
public partial class App : Application
{
}
}

12
sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml

@ -0,0 +1,12 @@
<Window x:Class="OpenIddict.Sandbox.Wpf.Client.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="OpenIddict WPF client" Height="450" Width="800">
<Grid>
<Button Content="Log in using the local server" HorizontalAlignment="Center" VerticalAlignment="Top" Click="LocalLoginButton_Click" Height="64" Width="252" FontSize="20" Margin="0,130,0,0" />
<Button Content="Log in using Twitter" HorizontalAlignment="Center" VerticalAlignment="Top" Click="TwitterLoginButton_Click" Height="64" Width="252" FontSize="20" Margin="0,234,0,0" />
</Grid>
</Window>

66
sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs

@ -0,0 +1,66 @@
using System.Windows;
using Dapplo.Microsoft.Extensions.Hosting.Wpf;
using OpenIddict.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Abstractions.OpenIddictExceptions;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Sandbox.Wpf.Client
{
public partial class MainWindow : Window, IWpfShell
{
private readonly OpenIddictClientService _service;
public MainWindow(OpenIddictClientService service)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
InitializeComponent();
}
private async void LocalLoginButton_Click(object sender, RoutedEventArgs e)
=> await AuthenticateAsync("Local");
private async void TwitterLoginButton_Click(object sender, RoutedEventArgs e)
=> await AuthenticateAsync(Providers.Twitter);
private async Task AuthenticateAsync(string provider)
{
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90));
try
{
// Ask OpenIddict to initiate the challenge and launch the system browser
// to allow the user to complete the interactive authentication dance.
var nonce = await _service.ChallengeWithBrowserAsync(
provider, cancellationToken: source.Token);
// Wait until the user approved or rejected the authorization
// demand and retrieve the resulting claims-based principal.
var (_, _, principal) = await _service.AuthenticateWithBrowserAsync(
nonce, cancellationToken: source.Token);
MessageBox.Show($"Welcome, {principal.FindFirst(Claims.Name)!.Value}.",
"Authentication successful", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
MessageBox.Show("The authentication process was aborted.",
"Authentication timed out", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied)
{
MessageBox.Show("The authorization was denied by the end user.",
"Authorization denied", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch
{
MessageBox.Show("An error occurred while trying to authenticate the user.",
"Authentication failed", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}

32
sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net48;net7.0-windows</TargetFrameworks>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UseWPF>true</UseWPF>
<IsShipping>false</IsShipping>
<SignAssembly>false</SignAssembly>
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.AppServices" />
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.Wpf" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<Using Remove="System.Net.Http" />
</ItemGroup>
</Project>

99
sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs

@ -0,0 +1,99 @@
using System.IO;
using Dapplo.Microsoft.Extensions.Hosting.Wpf;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Client;
using OpenIddict.Sandbox.Wpf.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
var host = Host.CreateDefaultBuilder(args)
// Note: applications for which a single instance is preferred can reference
// the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this
// method to automatically close extra instances based on the specified identifier:
//
// .ConfigureSingleInstance(options => options.MutexId = "{C587B9EA-A870-4CF3-8B00-33DF67FCA143}")
//
.ConfigureServices(services =>
{
services.AddDbContext<DbContext>(options =>
{
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-wpf-client.sqlite3")}");
options.UseOpenIddict();
});
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict client components.
.AddClient(options =>
{
// Note: this sample uses the authorization code and refresh token
// flows, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the Windows host.
options.UseWindows();
// Set the client URI that will uniquely identify this application.
options.SetClientUri(new Uri("openiddict-sandbox-wpf-client://localhost/", UriKind.Absolute));
// Register the System.Net.Http integration and use the identity of the current
// assembly as a more specific user agent, which can be useful when dealing with
// providers that use the user agent as a way to throttle requests (e.g Reddit).
options.UseSystemNetHttp()
.SetProductInformation(typeof(Program).Assembly);
// Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration
{
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
ProviderName = "Local",
ClientId = "wpf",
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
});
// Register the Web providers integrations.
//
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative));
});
// Register the worker responsible for creating the database used to store tokens
// and adding the registry entries required to register the custom URI scheme.
//
// Note: in a real world application, this step should be part of a setup script.
services.AddHostedService<Worker>();
})
.ConfigureWpf(options =>
{
options.UseApplication<App>();
options.UseWindow<MainWindow>();
})
.UseWpfLifetime()
.Build();
await host.RunAsync();

55
sandbox/OpenIddict.Sandbox.Wpf.Client/Worker.cs

@ -0,0 +1,55 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Win32;
namespace OpenIddict.Sandbox.Wpf.Client;
public class Worker : IHostedService
{
private readonly IServiceProvider _provider;
public Worker(IServiceProvider provider)
=> _provider = provider;
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
RegistryKey? root = null;
// Create the registry entries necessary to handle URI protocol activations.
// Note: the application MUST be run once as an administrator for this to work.
try
{
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-wpf-client");
if (root is null)
{
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-wpf-client");
root.SetValue(string.Empty, "URL:openiddict-sandbox-wpf-client");
root.SetValue("URL Protocol", string.Empty);
using var command = root.CreateSubKey("shell\\open\\command");
command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"",
#if SUPPORTS_ENVIRONMENT_PROCESS_PATH
Environment.ProcessPath
#else
Process.GetCurrentProcess().MainModule.FileName
#endif
));
}
}
finally
{
root?.Dispose();
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -130,6 +130,7 @@ public static class OpenIddictConstants
public const string GrantType = "oi_grt_typ";
public const string HostProperties = "oi_hst_props";
public const string IdentityTokenLifetime = "oi_idt_lft";
public const string InstanceId = "oi_instc_id";
public const string Issuer = "oi_iss";
public const string Nonce = "oi_nce";
public const string PostLogoutRedirectUri = "oi_pstlgt_reduri";

43
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -547,7 +547,7 @@ To register the server services, use 'services.AddOpenIddict().AddServer()'.</va
<value>The issuer cannot be null or empty.</value>
</data>
<data name="ID0127" xml:space="preserve">
<value>The base URI or request URI cannot be retrieved from the request context or are now valid absolute URIs.</value>
<value>The base URI or request URI cannot be retrieved from the request context or are not valid absolute URIs.</value>
</data>
<data name="ID0128" xml:space="preserve">
<value>An OAuth 2.0/OpenID Connect server configuration or an issuer URI must be registered.
@ -896,7 +896,7 @@ To register the validation services, use 'services.AddOpenIddict().AddValidation
<value>One or more validation error(s) occurred while trying to create a new token:</value>
</data>
<data name="ID0226" xml:space="preserve">
<value>An error occurred while trying to create a new token</value>
<value>An error occurred while trying to create a new token.</value>
</data>
<data name="ID0227" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to update an existing token:</value>
@ -1420,6 +1420,39 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0373" xml:space="preserve">
<value>Only instances of type '{0}' can be used as primary HTTP handlers for the HTTP clients managed by OpenIddict.</value>
</data>
<data name="ID0374" xml:space="preserve">
<value>An error occurred while authenticating the user.</value>
</data>
<data name="ID0375" xml:space="preserve">
<value>The protocol activation arguments cannot be resolved from the client transaction.</value>
</data>
<data name="ID0376" xml:space="preserve">
<value>The identifier of the application instance that initiated the authentication process cannot be resolved from the state token.</value>
</data>
<data name="ID0377" xml:space="preserve">
<value>Marshalling of authentication demands is not supported on ASP.NET Core and OWIN. To retrieve the authentication result, use 'IAuthenticationService.AuthenticateAsync()' or 'AuthenticationManager.AuthenticateAsync()'.</value>
</data>
<data name="ID0378" xml:space="preserve">
<value>An error occurred while adding the challenge operation to the list of tracked demands, which may indicate a nonce collision. Make sure nonces are unique, contain enough entropy and are generated using a crypto-secure random number generator.</value>
</data>
<data name="ID0379" xml:space="preserve">
<value>The specified nonce is already used to track another authentication operation.</value>
</data>
<data name="ID0380" xml:space="preserve">
<value>An error occurred while marking the authentication operation as completed, which may indicate a nonce collision. Make sure nonces are unique, contain enough entropy and are generated using a crypto-secure random number generator.</value>
</data>
<data name="ID0381" xml:space="preserve">
<value>An error occurred while removing the authentication operation from the list of tracked demands, which may indicate a nonce collision. Make sure nonces are unique, contain enough entropy and are generated using a crypto-secure random number generator.</value>
</data>
<data name="ID0382" xml:space="preserve">
<value>An error occurred while marking the authentication operation as failed, which may indicate a nonce collision. Make sure nonces are unique, contain enough entropy and are generated using a crypto-secure random number generator.</value>
</data>
<data name="ID0383" xml:space="preserve">
<value>An error occurred while waiting for the authentication operation to complete, which may indicate a nonce collision. Make sure nonces are unique, contain enough entropy and are generated using a crypto-secure random number generator.</value>
</data>
<data name="ID0384" xml:space="preserve">
<value>An explicit client URI must be set when using the OpenIddict client Windows integration. To set the client URI, use 'services.AddOpenIddict().AddClient().SetClientUri()'.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -1972,6 +2005,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID4018" xml:space="preserve">
<value>The authorization identifier shouldn't be null or empty at this point.</value>
</data>
<data name="ID4019" xml:space="preserve">
<value>The nonce shouldn't be null or empty at this point.</value>
</data>
<data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value>
</data>
@ -2572,6 +2608,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6212" xml:space="preserve">
<value>The authorization request was rejected because the '{ResponseType}' response type is not a valid combination.</value>
</data>
<data name="ID6213" xml:space="preserve">
<value>An error occurred while handling an inter-process message.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

17
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs

@ -60,7 +60,11 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
Context.Features.Set(new OpenIddictClientAspNetCoreFeature { Transaction = transaction });
}
var context = new ProcessRequestContext(transaction);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = Context.RequestAborted
};
await _dispatcher.DispatchAsync(context);
if (context.IsRequestHandled)
@ -77,6 +81,7 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -113,8 +118,10 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName!);
if (context is null)
{
context = new ProcessAuthenticationContext(transaction);
await _dispatcher.DispatchAsync(context);
await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = Context.RequestAborted
});
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
@ -345,6 +352,7 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = Context.RequestAborted,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Request = new OpenIddictRequest()
};
@ -360,6 +368,7 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -389,6 +398,7 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
var context = new ProcessSignOutContext(transaction)
{
CancellationToken = Context.RequestAborted,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Request = new OpenIddictRequest()
};
@ -406,6 +416,7 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,

36
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -40,6 +40,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
/*
* Authentication processing:
*/
ValidateAuthenticationNonce.Descriptor,
ResolveRequestForgeryProtection.Descriptor,
/*
@ -293,6 +294,40 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands that specify an explicit nonce property.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public sealed class ValidateAuthenticationNonce : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ValidateAuthenticationNonce>()
.SetOrder(ValidateAuthenticationDemand.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0377));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the request forgery protection from the correlation cookie.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
@ -310,6 +345,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveRequestForgeryProtection>()
.SetOrder(ValidateRequestForgeryProtection.Descriptor.Order - 500)

17
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs

@ -53,7 +53,11 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
Context.Set(typeof(OpenIddictClientTransaction).FullName, transaction);
}
var context = new ProcessRequestContext(transaction);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = Request.CallCancelled
};
await _dispatcher.DispatchAsync(context);
// Store the context in the transaction so that it can be retrieved from InvokeAsync().
@ -87,6 +91,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -123,8 +128,10 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName!);
if (context is null)
{
context = new ProcessAuthenticationContext(transaction);
await _dispatcher.DispatchAsync(context);
await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = Request.CallCancelled
});
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
@ -290,6 +297,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = Request.CallCancelled,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Request = new OpenIddictRequest()
};
@ -305,6 +313,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -332,6 +341,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
var context = new ProcessSignOutContext(transaction)
{
CancellationToken = Request.CallCancelled,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Request = new OpenIddictRequest()
};
@ -347,6 +357,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,

36
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -35,6 +35,7 @@ public static partial class OpenIddictClientOwinHandlers
/*
* Authentication processing:
*/
ValidateAuthenticationNonce.Descriptor,
ResolveRequestForgeryProtection.Descriptor,
/*
@ -294,6 +295,40 @@ public static partial class OpenIddictClientOwinHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands that specify an explicit nonce property.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public sealed class ValidateAuthenticationNonce : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ValidateAuthenticationNonce>()
.SetOrder(ValidateAuthenticationDemand.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0377));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the request forgery protection from the correlation cookie.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
@ -311,6 +346,7 @@ public static partial class OpenIddictClientOwinHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveRequestForgeryProtection>()
.SetOrder(ValidateStateToken.Descriptor.Order + 500)

18
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -296,15 +296,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers
(context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken) = context.Registration.ProviderName switch
{
// While PayPal claims the OpenID Connect flavor of the code flow is supported,
// their implementation doesn't return an id_token from the token endpoint.
Providers.PayPal => (false, false, false),
_ => (context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken)
};
{
// While PayPal claims the OpenID Connect flavor of the code flow is supported,
// their implementation doesn't return an id_token from the token endpoint.
Providers.PayPal => (false, false, false),
_ => (context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken)
};
return default;
}

41
src/OpenIddict.Client.Windows/OpenIddict.Client.Windows.csproj

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp3.1;net6.0-windows;net7.0-windows</TargetFrameworks>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
<PropertyGroup>
<Description>Windows integration package for the OpenIddict client services.</Description>
<PackageTags>$(PackageTags);client;windows</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Client\OpenIddict.Client.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
</ItemGroup>
<ItemGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '5.0'))) ">
<PackageReference Include="NamedPipeServerStream.NetFrameworkVersion" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\shared\OpenIddict.Extensions\*\*.cs" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
<Using Include="OpenIddict.Client.OpenIddictClientEvents" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlers" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlerFilters" Static="true" />
<Using Include="OpenIddict.Client.Windows.OpenIddictClientWindowsHandlers" Static="true" />
<Using Include="OpenIddict.Client.Windows.OpenIddictClientWindowsHandlerFilters" Static="true" />
</ItemGroup>
</Project>

29
src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs

@ -0,0 +1,29 @@
/*
* 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.Collections.Immutable;
using System.ComponentModel;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Represents a Windows application activation.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientWindowsActivation
{
/// <summary>
/// Gets or sets the command line arguments used to
/// launch the current instance of the application.
/// </summary>
public ImmutableArray<string> CommandLineArguments { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the activation
/// was redirected from another instance of the application.
/// </summary>
public bool IsActivationRedirected { get; set; }
}

131
src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs

@ -0,0 +1,131 @@
/*
* 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.ComponentModel;
using System.IO.Pipes;
using OpenIddict.Client.Windows;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Exposes the necessary methods required to configure
/// the OpenIddict client Windows integration.
/// </summary>
public sealed class OpenIddictClientWindowsBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictClientWindowsBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictClientWindowsBuilder(IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict client Windows configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public OpenIddictClientWindowsBuilder Configure(Action<OpenIddictClientWindowsOptions> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Sets the timeout after which authentication demands that
/// are not completed are automatically aborted by OpenIddict.
/// </summary>
/// <param name="timeout">The authentication timeout.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public OpenIddictClientWindowsBuilder SetAuthenticationTimeout(TimeSpan timeout)
=> Configure(options => options.AuthenticationTimeout = timeout);
/// <summary>
/// Sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.
/// </summary>
/// <param name="identifier">The identifier of the current instance.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientWindowsBuilder SetInstanceIdentifier(string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier));
}
return Configure(options => options.InstanceIdentifier = identifier);
}
/// <summary>
/// Sets the base name of the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="name">The name of the pipe.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientWindowsBuilder SetPipeName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(SR.FormatID0366(nameof(name)), nameof(name));
}
return Configure(options => options.PipeName = name);
}
/// <summary>
/// Sets the security policy applied to the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="security">The security policy applied to the pipe.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientWindowsBuilder SetPipeSecurity(PipeSecurity security)
{
if (security is null)
{
throw new ArgumentNullException(nameof(security));
}
return Configure(options => options.PipeSecurity = security);
}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><see langword="true"/> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => base.GetHashCode();
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString() => base.ToString();
}

108
src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs

@ -0,0 +1,108 @@
/*
* 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.ComponentModel;
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
#if !SUPPORTS_HOST_ENVIRONMENT
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment;
#endif
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the methods required to ensure that the OpenIddict client configuration is valid.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientWindowsConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientWindowsOptions>
{
private readonly IHostEnvironment _environment;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsConfiguration"/> class.
/// </summary>
/// <param name="environment">The host environment.</param>
public OpenIddictClientWindowsConfiguration(IHostEnvironment environment)
=> _environment = environment ?? throw new ArgumentNullException(nameof(environment));
/// <inheritdoc/>
public void Configure(OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict Windows client components.
options.Handlers.AddRange(OpenIddictClientWindowsHandlers.DefaultHandlers);
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Ensure an explicit client URI was set when using the Windows integration.
if (options.ClientUri is not { IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384));
}
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientWindowsOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// If no explicit instance identifier was specified, use a random GUID.
if (string.IsNullOrEmpty(options.InstanceIdentifier))
{
options.InstanceIdentifier = Guid.NewGuid().ToString();
}
// If no explicit pipe name was specified, compute the SHA-256 hash of the
// application name resolved from the host and use it as a unique identifier.
//
// Note: the pipe name is deliberately prefixed with "LOCAL\" to support
// partial trust/sandboxed applications that are executed in an AppContainer
// and cannot communicate with applications outside the sandbox container.
if (string.IsNullOrEmpty(options.PipeName))
{
options.PipeName = $@"LOCAL\OpenIddict.Client.Windows\{
Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(
Encoding.UTF8.GetBytes(_environment.ApplicationName)))
}";
}
// If no explicit pipe security policy was specified, grant the current user
// full control over the created pipe and allow cross-process communication
// between elevated and non-elevated processes.
if (options.PipeSecurity is null)
{
using var identity = WindowsIdentity.GetCurrent();
options.PipeSecurity = new PipeSecurity();
options.PipeSecurity.SetOwner(identity.User!);
options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!,
PipeAccessRights.FullControl, AccessControlType.Allow));
}
}
}

25
src/OpenIddict.Client.Windows/OpenIddictClientWindowsConstants.cs

@ -0,0 +1,25 @@
/*
* 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.Client.Windows;
/// <summary>
/// Exposes common constants used by the OpenIddict Windows host.
/// </summary>
public static class OpenIddictClientWindowsConstants
{
public static class Tokens
{
public const string AuthorizationCode = "authorization_code";
public const string BackchannelAccessToken = "backchannel_access_token";
public const string BackchannelIdentityToken = "backchannel_id_token";
public const string FrontchannelAccessToken = "frontchannel_access_token";
public const string FrontchannelIdentityToken = "frontchannel_id_token";
public const string RefreshToken = "refresh_token";
public const string StateToken = "state_token";
public const string UserinfoToken = "userinfo_token";
}
}

95
src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs

@ -0,0 +1,95 @@
/*
* 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 Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using OpenIddict.Client;
using OpenIddict.Client.Windows;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Exposes extensions allowing to register the OpenIddict client services.
/// </summary>
public static class OpenIddictClientWindowsExtensions
{
/// <summary>
/// Registers the OpenIddict client services for Windows in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public static OpenIddictClientWindowsBuilder UseWindows(this OpenIddictClientBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
// Note: the OpenIddict IHostedService implementation is deliberately registered as early as possible to
// ensure protocol activations can be handled before another service can stop the initialization of the
// application (e.g Dapplo.Microsoft.Extensions.Hosting.AppServices relies on an IHostedService to implement
// single instantiation, which would prevent the OpenIddict service from handling the protocol activation
// if the OpenIddict IHostedService implementation was not registered before the Dapplo IHostedService).
if (!builder.Services.Any(static descriptor => descriptor.ServiceType == typeof(IHostedService) &&
descriptor.ImplementationType == typeof(OpenIddictClientWindowsService)))
{
builder.Services.Insert(0, ServiceDescriptor.Singleton<IHostedService, OpenIddictClientWindowsService>());
}
// Register the marshaller responsible for managing authentication operations.
builder.Services.TryAddSingleton<OpenIddictClientWindowsMarshaller>();
// Register the built-in filters used by the default OpenIddict Windows client event handlers.
builder.Services.TryAddSingleton<RequireAuthenticationNonce>();
builder.Services.TryAddSingleton<RequireInteractiveSession>();
builder.Services.TryAddSingleton<RequireWindowsActivation>();
// Register the built-in event handlers used by the OpenIddict Windows client components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(OpenIddictClientWindowsHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Register the option initializer and the background service used by the OpenIddict Windows client integration services.
// Note: TryAddEnumerable() is used here to ensure the initializers and the background service are only registered once.
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IHostedService, OpenIddictClientWindowsListener>(),
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWindowsConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWindowsConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientWindowsOptions>, OpenIddictClientWindowsConfiguration>()
});
return new OpenIddictClientWindowsBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict client services for Windows in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the client services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public static OpenIddictClientBuilder UseWindows(
this OpenIddictClientBuilder builder, Action<OpenIddictClientWindowsBuilder> configuration)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseWindows());
return builder;
}
}

68
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs

@ -0,0 +1,68 @@
/*
* 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.ComponentModel;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains a collection of event handler filters commonly used by the Windows handlers.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientWindowsHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers
/// if no explicit nonce was attached to the authentication context.
/// </summary>
public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.Nonce));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no interactive user session was detected.
/// </summary>
public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(Environment.UserInteractive);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no Windows activation was found.
/// </summary>
public sealed class RequireWindowsActivation : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.Transaction.GetWindowsActivation() is not null);
}
}
}

82
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.Authentication.cs

@ -0,0 +1,82 @@
/*
* 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.Collections.Immutable;
using System.Diagnostics;
using Microsoft.Extensions.Primitives;
using OpenIddict.Extensions;
namespace OpenIddict.Client.Windows;
public static partial class OpenIddictClientWindowsHandlers
{
public static class Authentication
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authorization request processing:
*/
LaunchSystemBrowser.Descriptor,
/*
* Redirection request extraction:
*/
ExtractRequestUriParameters<ExtractRedirectionRequestContext>.Descriptor,
/*
* Redirection request handling:
*/
ProcessResponse<HandleRedirectionRequestContext>.Descriptor,
/*
* Redirection response handling:
*/
ProcessResponse<ApplyRedirectionResponseContext>.Descriptor);
/// <summary>
/// Contains the logic responsible for initiating authorization requests using the system browser.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by Windows.
/// </summary>
public class LaunchSystemBrowser : IOpenIddictClientHandler<ApplyAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyAuthorizationRequestContext>()
.AddFilter<RequireInteractiveSession>()
.UseSingletonHandler<LaunchSystemBrowser>()
.SetOrder(50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
var uri = OpenIddictHelpers.AddQueryStringParameters(
new Uri(context.AuthorizationEndpoint, UriKind.Absolute),
context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value)));
Process.Start(new ProcessStartInfo
{
FileName = uri.AbsoluteUri,
UseShellExecute = true
});
return default;
}
}
}
}

1320
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs

File diff suppressed because it is too large

21
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs

@ -0,0 +1,21 @@
/*
* 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.Client.Windows;
/// <summary>
/// Exposes companion extensions for the OpenIddict/Windows integration.
/// </summary>
public static class OpenIddictClientWindowsHelpers
{
/// <summary>
/// Gets the <see cref="OpenIddictClientWindowsActivation"/> associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="OpenIddictClientWindowsActivation"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictClientWindowsActivation? GetWindowsActivation(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<OpenIddictClientWindowsActivation>(typeof(OpenIddictClientWindowsActivation).FullName!);
}

161
src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs

@ -0,0 +1,161 @@
/*
* 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.Collections.Immutable;
using System.ComponentModel;
using System.IO.Pipes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
#if !SUPPORTS_HOST_APPLICATION_LIFETIME
using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime;
#endif
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the logic necessary to handle URI protocol activations that
/// are redirected by other instances using inter-process communication.
/// </summary>
/// <remarks>
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientWindowsService"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsListener : BackgroundService
{
private readonly ILogger<OpenIddictClientWindowsListener> _logger;
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options;
private readonly IServiceProvider _provider;
public OpenIddictClientWindowsListener(
ILogger<OpenIddictClientWindowsListener> logger,
IOptionsMonitor<OpenIddictClientWindowsOptions> options,
IServiceProvider provider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var buffer = new MemoryStream();
using var reader = new BinaryReader(buffer);
#if SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL
using var stream = new NamedPipeServerStream(
#elif SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
using var stream = NamedPipeServerStreamAcl.Create(
#else
using var stream = NamedPipeServerStreamConstructors.New(
#endif
pipeName : $@"{_options.CurrentValue.PipeName}\{_options.CurrentValue.InstanceIdentifier}",
direction : PipeDirection.In,
maxNumberOfServerInstances: 1,
transmissionMode : PipeTransmissionMode.Message,
options : PipeOptions.Asynchronous,
inBufferSize : 0,
outBufferSize : 0,
pipeSecurity : _options.CurrentValue.PipeSecurity,
inheritability : HandleInheritability.None,
additionalAccessRights : 0);
// Wait for a writer to connect to the named pipe,
// copy its content to the memory stream and rewind it.
await stream.WaitForConnectionAsync(stoppingToken);
await stream.CopyToAsync(buffer, bufferSize: 81_920, stoppingToken);
buffer.Seek(0L, SeekOrigin.Begin);
var scope = _provider.CreateScope();
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
switch (reader.ReadInt32())
{
case 0x01: // Protocol activations
{
// Ensure the binary serialization format is supported.
var version = reader.ReadInt32();
if (version is not 0x01)
{
continue;
}
var length = reader.ReadInt32();
if (length is not > 0)
{
continue;
}
var builder = ImmutableArray.CreateBuilder<string>(length);
for (var index = 0; index < length; index++)
{
builder.Add(reader.ReadString());
}
// Create a client transaction and store the command line arguments so they can be
// retrieved by the Windows-specific client event handlers that need to access them.
var transaction = await factory.CreateTransactionAsync();
transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!,
new OpenIddictClientWindowsActivation
{
CommandLineArguments = builder.MoveToImmutable(),
IsActivationRedirected = true
});
var context = new ProcessRequestContext(transaction)
{
CancellationToken = stoppingToken
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction)
{
CancellationToken = stoppingToken,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
});
}
break;
}
}
}
// Swallow all exceptions to ensure the service doesn't exit when encountering an exception.
catch (Exception exception)
{
_logger.LogWarning(exception, SR.GetResourceString(SR.ID6213));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
}
}

130
src/OpenIddict.Client.Windows/OpenIddictClientWindowsMarshaller.cs

@ -0,0 +1,130 @@
/*
* 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.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the APIs needed to coordinate authentication operations that happen in a different context.
/// </summary>
public sealed class OpenIddictClientWindowsMarshaller
{
private readonly ConcurrentDictionary<string, Lazy<(
string RequestForgeryProtection,
SemaphoreSlim Semaphore,
TaskCompletionSource<ProcessAuthenticationContext> TaskCompletionSource)>> _operations = new();
/// <summary>
/// Determines whether the authentication demand corresponding to the specified nonce is tracked.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <returns><see langword="true"/> if the operation is tracked, <see langword="false"/> otherwise.</returns>
internal bool IsTracked(string nonce) => _operations.ContainsKey(nonce);
/// <summary>
/// Tries to add the specified authentication demand to the list of tracked operations.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="protection">The request forgery protection associated with the specified authentication demand.</param>
/// <returns><see langword="true"/> if the operation could be added, <see langword="false"/> otherwise.</returns>
internal bool TryAdd(string nonce, string protection)
=> _operations.TryAdd(nonce, new(() => (protection, new SemaphoreSlim(initialCount: 1, maxCount: 1), new())));
/// <summary>
/// Tries to acquire a lock on the authentication demand corresponding to the specified nonce.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <returns><see langword="true"/> if the lock could be taken, <see langword="false"/> otherwise.</returns>
internal bool TryAcquireLock(string nonce)
=> _operations.TryGetValue(nonce, out var operation) && operation.Value.Semaphore.Wait(TimeSpan.Zero);
/// <summary>
/// Tries to resolve the authentication context associated with the specified nonce.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="context">The authentication context associated with the tracked operation.</param>
/// <returns><see langword="true"/> if the context could be resolved, <see langword="false"/> otherwise.</returns>
internal bool TryGetResult(string nonce, [NotNullWhen(true)] out ProcessAuthenticationContext? context)
{
if (!_operations.TryGetValue(nonce, out var operation))
{
context = null;
return false;
}
if (!operation.IsValueCreated || !operation.Value.TaskCompletionSource.Task.IsCompleted)
{
context = null;
return false;
}
context = operation.Value.TaskCompletionSource.Task.Result;
return true;
}
/// <summary>
/// Tries to wait for the authentication demand associated with the specified nonce to complete.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><see langword="true"/> if the authentication demand is tracked, <see langword="false"/> otherwise.</returns>
/// <exception cref="OperationCanceledException">The operation was canceled by the user.</exception>
internal async Task<bool> TryWaitForCompletionAsync(string nonce, CancellationToken cancellationToken)
{
if (!_operations.TryGetValue(nonce, out var operation))
{
return false;
}
var source = new TaskCompletionSource<bool>(TaskCreationOptions.None);
using (cancellationToken.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source))
{
if (await Task.WhenAny(operation.Value.TaskCompletionSource.Task, source.Task) == source.Task)
{
throw new OperationCanceledException(cancellationToken);
}
await operation.Value.TaskCompletionSource.Task;
return true;
}
}
/// <summary>
/// Tries to resolve the request forgery protection associated with the specified authentication demand.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="protection">The request forgery protection associated with the specified authentication demand.</param>
/// <returns><see langword="true"/> if the operation could be validated, <see langword="false"/> otherwise.</returns>
internal bool TryGetRequestForgeryProtection(string nonce, [NotNullWhen(true)] out string? protection)
{
if (_operations.TryGetValue(nonce, out var operation))
{
protection = operation.Value.RequestForgeryProtection;
return true;
}
protection = null;
return false;
}
/// <summary>
/// Tries to complete the specified authentication demand.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="context">The authentication context that will be returned to the caller.</param>
/// <returns><see langword="true"/> if the operation could be completed, <see langword="false"/> otherwise.</returns>
internal bool TryComplete(string nonce, ProcessAuthenticationContext context)
=> _operations.TryGetValue(nonce, out var operation) && operation.Value.TaskCompletionSource.TrySetResult(context);
/// <summary>
/// Tries to remove the specified authentication operation from the list of tracked operations.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <returns><see langword="true"/> if the operation could be removed, <see langword="false"/> otherwise.</returns>
internal bool TryRemove(string nonce) => _operations.TryRemove(nonce, out _);
}

50
src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs

@ -0,0 +1,50 @@
/*
* 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.IO.Pipes;
using Microsoft.Extensions.Hosting;
#if !SUPPORTS_HOST_ENVIRONMENT
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment;
#endif
namespace OpenIddict.Client.Windows;
/// <summary>
/// Provides various settings needed to configure the OpenIddict Windows client integration.
/// </summary>
public sealed class OpenIddictClientWindowsOptions
{
/// <summary>
/// Gets or sets the timeout after which authentication demands
/// that are not completed are automatically aborted by OpenIddict.
/// </summary>
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Gets or sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.
/// </summary>
public string? InstanceIdentifier { get; set; }
/// <summary>
/// Gets or sets the base name of the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default name is automatically computed.
/// </remarks>
public string PipeName { get; set; } = default!;
/// <summary>
/// Gets or sets the security policy applied to the pipe created by OpenIddict
/// to enable inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default policy is automatically created.
/// </remarks>
public PipeSecurity PipeSecurity { get; set; } = default!;
}

103
src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs

@ -0,0 +1,103 @@
/*
* 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.Collections.Immutable;
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
#if !SUPPORTS_HOST_APPLICATION_LIFETIME
using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime;
#endif
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the logic necessary to handle initial URI protocol activations.
/// </summary>
/// <remarks>
/// Note: redirected URI protocol activations are handled by <see cref="OpenIddictClientWindowsListener"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsService : IHostedService
{
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsService"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
public OpenIddictClientWindowsService(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
public async Task StartAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Note: initial URI protocol activation handling is implemented as a regular IHostedService
// rather than as a BackgroundService to allow blocking the initialization of the host until
// the activation is fully processed by the OpenIddict pipeline. By doing that, the UI thread
// is not started until redirection requests (like authorization responses) are fully processed,
// which allows handling these requests transparently and helps avoid the "flashing window effect":
// once a request has been handled by the OpenIddict pipeline, a dedicated handler is responsible
// for stopping the application gracefully using the IHostApplicationLifetime.StopApplication() API.
var scope = _provider.CreateScope();
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
// Create a client transaction and store the command line arguments so they can be
// retrieved by the Windows-specific client event handlers that need to access them.
var transaction = await factory.CreateTransactionAsync();
transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!,
new OpenIddictClientWindowsActivation
{
CommandLineArguments = ImmutableArray.CreateRange(Environment.GetCommandLineArgs()),
IsActivationRedirected = false
});
var context = new ProcessRequestContext(transaction)
{
CancellationToken = cancellationToken
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction)
{
CancellationToken = cancellationToken,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
});
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

20
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -30,6 +30,21 @@ public static partial class OpenIddictClientEvents
/// </summary>
public OpenIddictClientTransaction Transaction { get; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
/// <remarks>
/// Note: for security reasons, this property shouldn't be used by event
/// handlers to abort security-sensitive operations. As such, it is
/// recommended to use this property only for user-dependent operations.
/// </remarks>
public CancellationToken CancellationToken
{
get => Transaction.CancellationToken;
set => Transaction.CancellationToken = value;
}
/// <summary>
/// Gets or sets the endpoint type that handled the request, if applicable.
/// </summary>
@ -308,6 +323,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the nonce used to identify the authentication demand, if applicable.
/// </summary>
public string? Nonce { get; set; }
/// <summary>
/// Gets or sets the issuer used for the authentication demand, if applicable.
/// </summary>

127
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -36,6 +36,7 @@ public static partial class OpenIddictClientHandlers
ResolveValidatedStateToken.Descriptor,
ValidateRequiredStateToken.Descriptor,
ValidateStateToken.Descriptor,
ResolveNonceFromStateToken.Descriptor,
RedeemStateTokenEntry.Descriptor,
ValidateStateTokenEndpointType.Descriptor,
ValidateRequestForgeryProtection.Descriptor,
@ -265,52 +266,55 @@ public static partial class OpenIddictClientHandlers
break;
case OpenIddictClientEndpointType.Unknown:
if (string.IsNullOrEmpty(context.GrantType))
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
if (string.IsNullOrEmpty(context.GrantType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
if (context.GrantType is not (
GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.Implicit or GrantTypes.Password or GrantTypes.RefreshToken))
{
throw new InvalidOperationException(SR.FormatID0310(context.GrantType));
}
if (context.GrantType is not (
GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.Implicit or GrantTypes.Password or GrantTypes.RefreshToken))
{
throw new InvalidOperationException(SR.FormatID0310(context.GrantType));
}
if (!context.Options.GrantTypes.Contains(context.GrantType))
{
throw new InvalidOperationException(SR.FormatID0359(context.GrantType));
}
if (!context.Options.GrantTypes.Contains(context.GrantType))
{
throw new InvalidOperationException(SR.FormatID0359(context.GrantType));
}
if (context.GrantType is GrantTypes.Password)
{
if (string.IsNullOrEmpty(context.Username))
if (context.GrantType is GrantTypes.Password)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0337));
if (string.IsNullOrEmpty(context.Username))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0337));
}
if (string.IsNullOrEmpty(context.Password))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0338));
}
}
if (string.IsNullOrEmpty(context.Password))
if (context.GrantType is GrantTypes.RefreshToken &&
string.IsNullOrEmpty(context.RefreshToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0338));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
}
}
if (context.GrantType is GrantTypes.RefreshToken &&
string.IsNullOrEmpty(context.RefreshToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
// If no issuer was explicitly attached and a single client is registered, use it.
// Otherwise, throw an exception to indicate that setting an explicit issuer
// is required when multiple clients are registered.
context.Issuer ??= context.Options.Registrations.Count switch
{
0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
1 => context.Options.Registrations[0].Issuer,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355))
};
}
// If no issuer was explicitly attached and a single client is registered, use it.
// Otherwise, throw an exception to indicate that setting an explicit issuer
// is required when multiple clients are registered.
context.Issuer ??= context.Options.Registrations.Count switch
{
0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
1 => context.Options.Registrations[0].Issuer,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355))
};
break;
default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0290));
@ -347,7 +351,7 @@ public static partial class OpenIddictClientHandlers
//
// Client registrations/configurations that need to be resolved as part of authentication demands
// triggered from the redirection or post-logout redirection requests are handled elsewhere.
if (context.EndpointType is not OpenIddictClientEndpointType.Unknown)
if (context.Issuer is null || context.EndpointType is not OpenIddictClientEndpointType.Unknown)
{
return;
}
@ -562,6 +566,45 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the nonce identifying
/// the authentication operation from the state token principal.
/// </summary>
public sealed class ResolveNonceFromStateToken : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveNonceFromStateToken>()
.SetOrder(ValidateStateToken.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the nonce from the state token principal and attach it to the context.
context.Nonce = context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce) switch
{
{ Length: > 0 } nonce => nonce,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0354))
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to the received state token.
/// Note: this handler is not used when the degraded mode is enabled.
@ -582,11 +625,12 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseScopedHandler<RedeemStateTokenEntry>()
// Note: this handler is deliberately executed early in the pipeline to ensure
// that the state token entry is always marked as redeemed even if the authentication
// demand is rejected later in the pipeline (e.g because an error was returned).
.SetOrder(ValidateStateToken.Descriptor.Order + 1_000)
.SetOrder(ResolveNonceFromStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -634,6 +678,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateStateTokenEndpointType>()
.SetOrder(RedeemStateTokenEntry.Descriptor.Order + 1_000)
.Build();
@ -683,6 +728,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateRequestForgeryProtection>()
.SetOrder(ValidateStateTokenEndpointType.Descriptor.Order + 1_000)
@ -745,6 +791,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ValidateRequestForgeryProtection.Descriptor.Order + 1_000)
@ -852,6 +899,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveClientRegistrationFromStateToken>()
.SetOrder(ValidateEndpointUri.Descriptor.Order + 1_000)
.Build();
@ -1087,6 +1135,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ResolveGrantTypeAndResponseTypeFromStateToken>()
.SetOrder(HandleFrontchannelErrorResponse.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -1609,6 +1658,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireFrontchannelIdentityTokenPrincipal>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateFrontchannelIdentityTokenNonce>()
.SetOrder(ValidateFrontchannelIdentityTokenPresenter.Descriptor.Order + 1_000)
.Build();
@ -1658,7 +1708,7 @@ public static partial class OpenIddictClientHandlers
switch ((
FrontchannelIdentityTokenNonce: context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce),
StateTokenNonce: context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
StateTokenNonce: context.Nonce))
{
// If no nonce is present in the state token, bypass the validation logic.
case { StateTokenNonce: null or { Length: not > 0 } }:
@ -2913,6 +2963,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireBackchannelIdentityTokenPrincipal>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateBackchannelIdentityTokenNonce>()
.SetOrder(ValidateBackchannelIdentityTokenPresenter.Descriptor.Order + 1_000)
.Build();
@ -2962,7 +3013,7 @@ public static partial class OpenIddictClientHandlers
switch ((
BackchannelIdentityTokenNonce: context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce),
StateTokenNonce: context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
StateTokenNonce: context.Nonce))
{
// If no nonce is present in the state token, bypass the validation logic.
case { StateTokenNonce: null or { Length: not > 0 } }:

246
src/OpenIddict.Client/OpenIddictClientService.cs

@ -26,6 +26,233 @@ public sealed class OpenIddictClientService
public OpenIddictClientService(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <summary>
/// Initiates an interactive user authentication demand.
/// </summary>
/// <param name="issuer">The issuer.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public ValueTask<string> ChallengeWithBrowserAsync(
Uri issuer, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? properties = null, CancellationToken cancellationToken = default)
{
if (issuer is null)
{
throw new ArgumentNullException(nameof(issuer));
}
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
var registration = options.CurrentValue.Registrations.Find(registration => registration.Issuer == issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
return ChallengeWithBrowserAsync(registration, scopes, parameters, properties, cancellationToken);
}
/// <summary>
/// Initiates an interactive user authentication demand.
/// </summary>
/// <param name="provider">The name of the provider (see <see cref="OpenIddictClientRegistration.ProviderName"/>).</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public ValueTask<string> ChallengeWithBrowserAsync(
string provider, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? properties = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(provider))
{
throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider));
}
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
var registration = options.CurrentValue.Registrations.Find(registration => string.Equals(
registration.ProviderName, provider, StringComparison.Ordinal)) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0348));
return ChallengeWithBrowserAsync(registration, scopes, parameters, properties, cancellationToken);
}
/// <summary>
/// Initiates an interactive user authentication demand.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
private async ValueTask<string> ChallengeWithBrowserAsync(
OpenIddictClientRegistration registration, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? properties = null, CancellationToken cancellationToken = default)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = cancellationToken,
Configuration = configuration,
Issuer = registration.Issuer,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Registration = registration,
Request = parameters is not null ? new(parameters) : new(),
};
if (scopes is { Length: > 0 })
{
context.Scopes.UnionWith(scopes);
}
if (properties is { Count: > 0 })
{
foreach (var property in properties)
{
context.Properties[property.Key] = property.Value;
}
}
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction)
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
});
}
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0352));
}
return context.Nonce;
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Completes the interactive authentication demand corresponding to the specified nonce.
/// </summary>
/// <param name="nonce">The nonce obtained after a challenge operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateWithBrowserAsync(
string nonce, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = cancellationToken,
Nonce = nonce
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
message: SR.GetResourceString(SR.ID0374),
context.Error, context.ErrorDescription, context.ErrorUri);
}
else
{
var principal = OpenIddictHelpers.CreateMergedPrincipal(
context.FrontchannelIdentityTokenPrincipal,
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal) ?? new ClaimsPrincipal(new ClaimsIdentity());
// Attach the identity of the authorization to the returned principal to allow resolving it even if no other
// claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.StateTokenPrincipal?.GetClaim(Claims.AuthorizationServer))
.SetClaim(Claims.Private.ProviderName, context.StateTokenPrincipal?.GetClaim(Claims.Private.ProviderName));
return (
AuthorizationResponse: context.Request is not null ? new(context.Request.GetParameters()) : new(),
TokenResponse : context.TokenResponse ?? new(),
Principal : principal);
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Authenticates using the client credentials grant and resolves the corresponding tokens.
/// </summary>
@ -128,6 +355,7 @@ public sealed class OpenIddictClientService
var context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = cancellationToken,
Configuration = configuration,
GrantType = GrantTypes.ClientCredentials,
Issuer = registration.Issuer,
@ -299,6 +527,7 @@ public sealed class OpenIddictClientService
var context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = cancellationToken,
Configuration = configuration,
GrantType = GrantTypes.Password,
Issuer = registration.Issuer,
@ -464,6 +693,7 @@ public sealed class OpenIddictClientService
var context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = cancellationToken,
Configuration = configuration,
GrantType = GrantTypes.RefreshToken,
Issuer = registration.Issuer,
@ -570,6 +800,7 @@ public sealed class OpenIddictClientService
{
var context = new PrepareConfigurationRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request
@ -591,6 +822,7 @@ public sealed class OpenIddictClientService
{
var context = new ApplyConfigurationRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request
@ -614,6 +846,7 @@ public sealed class OpenIddictClientService
{
var context = new ExtractConfigurationResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request
@ -639,6 +872,7 @@ public sealed class OpenIddictClientService
{
var context = new HandleConfigurationResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request,
@ -725,6 +959,7 @@ public sealed class OpenIddictClientService
{
var context = new PrepareCryptographyRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request
@ -746,6 +981,7 @@ public sealed class OpenIddictClientService
{
var context = new ApplyCryptographyRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request
@ -769,6 +1005,7 @@ public sealed class OpenIddictClientService
{
var context = new ExtractCryptographyResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request
@ -794,6 +1031,7 @@ public sealed class OpenIddictClientService
{
var context = new HandleCryptographyResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Registration = registration,
Request = request,
@ -888,6 +1126,7 @@ public sealed class OpenIddictClientService
{
var context = new PrepareTokenRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -910,6 +1149,7 @@ public sealed class OpenIddictClientService
{
var context = new ApplyTokenRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -934,6 +1174,7 @@ public sealed class OpenIddictClientService
{
var context = new ExtractTokenResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -960,6 +1201,7 @@ public sealed class OpenIddictClientService
{
var context = new HandleTokenResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -1049,6 +1291,7 @@ public sealed class OpenIddictClientService
{
var context = new PrepareUserinfoRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -1071,6 +1314,7 @@ public sealed class OpenIddictClientService
{
var context = new ApplyUserinfoRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -1095,6 +1339,7 @@ public sealed class OpenIddictClientService
{
var context = new ExtractUserinfoResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
@ -1121,6 +1366,7 @@ public sealed class OpenIddictClientService
{
var context = new HandleUserinfoResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,

11
src/OpenIddict.Client/OpenIddictClientTransaction.cs

@ -15,6 +15,17 @@ namespace OpenIddict.Client;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientTransaction
{
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
/// <remarks>
/// Note: for security reasons, this property shouldn't be used by event
/// handlers to abort security-sensitive operations. As such, it is
/// recommended to use this property only for user-dependent operations.
/// </remarks>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// </summary>

18
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -60,7 +60,11 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
Context.Features.Set(new OpenIddictServerAspNetCoreFeature { Transaction = transaction });
}
var context = new ProcessRequestContext(transaction);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = Context.RequestAborted
};
await _dispatcher.DispatchAsync(context);
if (context.IsRequestHandled)
@ -77,6 +81,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -113,8 +118,10 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName!);
if (context is null)
{
context = new ProcessAuthenticationContext(transaction);
await _dispatcher.DispatchAsync(context);
await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = Context.RequestAborted
});
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
@ -327,6 +334,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = Context.RequestAborted,
Response = new OpenIddictResponse()
};
@ -341,6 +349,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -377,6 +386,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
var context = new ProcessSignInContext(transaction)
{
CancellationToken = Context.RequestAborted,
Principal = user,
Response = new OpenIddictResponse()
};
@ -417,6 +427,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
var context = new ProcessSignOutContext(transaction)
{
CancellationToken = Context.RequestAborted,
Response = new OpenIddictResponse()
};
@ -433,6 +444,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,

19
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -52,7 +52,11 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
Context.Set(typeof(OpenIddictServerTransaction).FullName, transaction);
}
var context = new ProcessRequestContext(transaction);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = Request.CallCancelled
};
await _dispatcher.DispatchAsync(context);
// Store the context in the transaction so that it can be retrieved from InvokeAsync().
@ -86,6 +90,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -122,8 +127,10 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName!);
if (context is null)
{
context = new ProcessAuthenticationContext(transaction);
await _dispatcher.DispatchAsync(context);
await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = Request.CallCancelled
});
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
@ -283,6 +290,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = Request.CallCancelled,
Response = new OpenIddictResponse()
};
@ -297,6 +305,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -324,6 +333,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
var context = new ProcessSignInContext(transaction)
{
CancellationToken = Request.CallCancelled,
Principal = signin.Principal,
Response = new OpenIddictResponse()
};
@ -339,6 +349,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -366,6 +377,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
var context = new ProcessSignOutContext(transaction)
{
CancellationToken = Request.CallCancelled,
Response = new OpenIddictResponse()
};
@ -380,6 +392,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,

15
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -29,6 +29,21 @@ public static partial class OpenIddictServerEvents
/// </summary>
public OpenIddictServerTransaction Transaction { get; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
/// <remarks>
/// Note: for security reasons, this property shouldn't be used by event
/// handlers to abort security-sensitive operations. As such, it is
/// recommended to use this property only for user-dependent operations.
/// </remarks>
public CancellationToken CancellationToken
{
get => Transaction.CancellationToken;
set => Transaction.CancellationToken = value;
}
/// <summary>
/// Gets or sets the endpoint type that handled the request, if applicable.
/// </summary>

11
src/OpenIddict.Server/OpenIddictServerTransaction.cs

@ -15,6 +15,17 @@ namespace OpenIddict.Server;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerTransaction
{
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
/// <remarks>
/// Note: for security reasons, this property shouldn't be used by event
/// handlers to abort security-sensitive operations. As such, it is
/// recommended to use this property only for user-dependent operations.
/// </remarks>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// </summary>

15
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs

@ -56,7 +56,11 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle
Context.Features.Set(new OpenIddictValidationAspNetCoreFeature { Transaction = transaction });
}
var context = new ProcessRequestContext(transaction);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = Context.RequestAborted
};
await _dispatcher.DispatchAsync(context);
if (context.IsRequestHandled)
@ -73,6 +77,7 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -109,8 +114,10 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName!);
if (context is null)
{
context = new ProcessAuthenticationContext(transaction);
await _dispatcher.DispatchAsync(context);
await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = Context.RequestAborted
});
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
@ -206,6 +213,7 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = Context.RequestAborted,
Response = new OpenIddictResponse()
};
@ -220,6 +228,7 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Context.RequestAborted,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,

15
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs

@ -50,7 +50,11 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Open
Context.Set(typeof(OpenIddictValidationTransaction).FullName, transaction);
}
var context = new ProcessRequestContext(transaction);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = Request.CallCancelled
};
await _dispatcher.DispatchAsync(context);
// Store the context in the transaction so that it can be retrieved from InvokeAsync().
@ -84,6 +88,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Open
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
@ -120,8 +125,10 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Open
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName!);
if (context is null)
{
context = new ProcessAuthenticationContext(transaction);
await _dispatcher.DispatchAsync(context);
await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)
{
CancellationToken = Request.CallCancelled
});
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
@ -214,6 +221,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Open
var context = new ProcessChallengeContext(transaction)
{
CancellationToken = Request.CallCancelled,
Response = new OpenIddictResponse()
};
@ -228,6 +236,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Open
{
var notification = new ProcessErrorContext(transaction)
{
CancellationToken = Request.CallCancelled,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,

15
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -29,6 +29,21 @@ public static partial class OpenIddictValidationEvents
/// </summary>
public OpenIddictValidationTransaction Transaction { get; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
/// <remarks>
/// Note: for security reasons, this property shouldn't be used by event
/// handlers to abort security-sensitive operations. As such, it is
/// recommended to use this property only for user-dependent operations.
/// </remarks>
public CancellationToken CancellationToken
{
get => Transaction.CancellationToken;
set => Transaction.CancellationToken = value;
}
/// <summary>
/// Gets or sets the endpoint type that handled the request, if applicable.
/// </summary>

11
src/OpenIddict.Validation/OpenIddictValidationTransaction.cs

@ -15,6 +15,17 @@ namespace OpenIddict.Validation;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictValidationTransaction
{
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
/// <remarks>
/// Note: for security reasons, this property shouldn't be used by event
/// handlers to abort security-sensitive operations. As such, it is
/// recommended to use this property only for user-dependent operations.
/// </remarks>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// </summary>

8
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTestServer.cs

@ -8,7 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.TestHost;
using OpenIddict.Server.IntegrationTests;
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
using Microsoft.Extensions.Hosting;
#endif
@ -19,7 +19,7 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests;
/// </summary>
public class OpenIddictServerAspNetCoreIntegrationTestServer : OpenIddictServerIntegrationTestServer
{
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
public OpenIddictServerAspNetCoreIntegrationTestServer(IHost host)
{
Host = host;
@ -44,7 +44,7 @@ public class OpenIddictServerAspNetCoreIntegrationTestServer : OpenIddictServerI
=> new(new OpenIddictServerIntegrationTestClient(Server.CreateClient()));
public override
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
async
#endif
ValueTask DisposeAsync()
@ -52,7 +52,7 @@ public class OpenIddictServerAspNetCoreIntegrationTestServer : OpenIddictServerI
// Dispose of the underlying test server.
Server.Dispose();
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
// Stop and dispose of the underlying generic host.
await Host.StopAsync();
Host.Dispose();

8
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs

@ -531,12 +531,12 @@ public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServ
}
protected override
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
async
#endif
ValueTask<OpenIddictServerIntegrationTestServer> CreateServerAsync(Action<OpenIddictServerBuilder>? configuration = null)
{
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
var builder = new HostBuilder();
#else
var builder = new WebHostBuilder();
@ -559,7 +559,7 @@ public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServ
});
});
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
builder.ConfigureWebHost(options =>
{
options.UseTestServer();
@ -569,7 +569,7 @@ public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServ
builder.Configure(ConfigurePipeline);
#endif
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
var host = await builder.StartAsync();
return new OpenIddictServerAspNetCoreIntegrationTestServer(host);

8
test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTestServer.cs

@ -8,7 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.TestHost;
using OpenIddict.Validation.IntegrationTests;
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
using Microsoft.Extensions.Hosting;
#endif
@ -19,7 +19,7 @@ namespace OpenIddict.Validation.AspNetCore.IntegrationTests;
/// </summary>
public class OpenIddictValidationAspNetCoreIntegrationTestServer : OpenIddictValidationIntegrationTestServer
{
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
public OpenIddictValidationAspNetCoreIntegrationTestServer(IHost host)
{
Host = host;
@ -44,7 +44,7 @@ public class OpenIddictValidationAspNetCoreIntegrationTestServer : OpenIddictVal
=> new(new OpenIddictValidationIntegrationTestClient(Server.CreateClient()));
public override
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
async
#endif
ValueTask DisposeAsync()
@ -52,7 +52,7 @@ public class OpenIddictValidationAspNetCoreIntegrationTestServer : OpenIddictVal
// Dispose of the underlying test server.
Server.Dispose();
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
// Stop and dispose of the underlying generic host.
await Host.StopAsync();
Host.Dispose();

8
test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTests.cs

@ -114,12 +114,12 @@ public partial class OpenIddictValidationAspNetCoreIntegrationTests : OpenIddict
}
protected override
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
async
#endif
ValueTask<OpenIddictValidationIntegrationTestServer> CreateServerAsync(Action<OpenIddictValidationBuilder>? configuration = null)
{
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
var builder = new HostBuilder();
#else
var builder = new WebHostBuilder();
@ -140,7 +140,7 @@ public partial class OpenIddictValidationAspNetCoreIntegrationTests : OpenIddict
});
});
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
builder.ConfigureWebHost(options =>
{
options.UseTestServer();
@ -150,7 +150,7 @@ public partial class OpenIddictValidationAspNetCoreIntegrationTests : OpenIddict
builder.Configure(ConfigurePipeline);
#endif
#if SUPPORTS_GENERIC_HOST
#if SUPPORTS_WEB_INTEGRATION_IN_GENERIC_HOST
var host = await builder.StartAsync();
return new OpenIddictValidationAspNetCoreIntegrationTestServer(host);

Loading…
Cancel
Save