Browse Source

Bring back OpenIddict.Validation and introduce OpenIddict.Validation.AspNetCore, OpenIddict.Validation.Owin, OpenIddict.Validation.DataProtection, OpenIddict.Validation.ServerIntegration and OpenIddict.Validation.SystemNetHttp

pull/812/head
Kévin Chalet 7 years ago
committed by GitHub
parent
commit
204b87dbea
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 42
      OpenIddict.sln
  2. 6
      samples/Mvc.Server/Controllers/ResourceController.cs
  3. 12
      samples/Mvc.Server/Startup.cs
  4. 16
      src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs
  5. 25
      src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs
  6. 2
      src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj
  7. 1
      src/OpenIddict.Owin/OpenIddict.Owin.csproj
  8. 8
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  9. 25
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
  10. 2
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
  11. 8
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
  12. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  13. 5
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  14. 25
      src/OpenIddict.Server/OpenIddictServerConstants.cs
  15. 6
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  16. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  17. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  18. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  19. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  20. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  21. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  22. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  23. 76
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  24. 6
      src/OpenIddict.Server/OpenIddictServerProvider.cs
  25. 5
      src/OpenIddict.Server/OpenIddictServerTokenHandler.cs
  26. 25
      src/OpenIddict.Validation.AspNetCore/OpenIddict.Validation.AspNetCore.csproj
  27. 73
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs
  28. 98
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs
  29. 27
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs
  30. 21
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreDefaults.cs
  31. 88
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreExtensions.cs
  32. 20
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs
  33. 225
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs
  34. 38
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlerFilters.cs
  35. 303
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs
  36. 92
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs
  37. 17
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs
  38. 17
      src/OpenIddict.Validation.DataProtection/IOpenIddictValidationDataProtectionFormatter.cs
  39. 21
      src/OpenIddict.Validation.DataProtection/OpenIddict.Validation.DataProtection.csproj
  40. 105
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionBuilder.cs
  41. 64
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs
  42. 57
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs
  43. 78
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionExtensions.cs
  44. 183
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs
  45. 232
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs
  46. 31
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionOptions.cs
  47. 24
      src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj
  48. 73
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs
  49. 36
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConfiguration.cs
  50. 27
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs
  51. 21
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinDefaults.cs
  52. 87
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs
  53. 214
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  54. 36
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlerFilters.cs
  55. 302
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs
  56. 109
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHelpers.cs
  57. 51
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs
  58. 80
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs
  59. 23
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs
  60. 21
      src/OpenIddict.Validation.ServerIntegration/OpenIddict.Validation.ServerIntegration.csproj
  61. 72
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationBuilder.cs
  62. 65
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs
  63. 69
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationExtensions.cs
  64. 15
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationOptions.cs
  65. 23
      src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj
  66. 82
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs
  67. 74
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  68. 16
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs
  69. 80
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs
  70. 300
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  71. 28
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs
  72. 28
      src/OpenIddict.Validation/IOpenIddictValidationHandler.cs
  73. 17
      src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs
  74. 18
      src/OpenIddict.Validation/IOpenIddictValidationProvider.cs
  75. 22
      src/OpenIddict.Validation/OpenIddict.Validation.csproj
  76. 657
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  77. 138
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  78. 19
      src/OpenIddict.Validation/OpenIddictValidationEndpointType.cs
  79. 271
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  80. 83
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  81. 39
      src/OpenIddict.Validation/OpenIddictValidationHandler.cs
  82. 199
      src/OpenIddict.Validation/OpenIddictValidationHandlerDescriptor.cs
  83. 66
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  84. 533
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  85. 86
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  86. 132
      src/OpenIddict.Validation/OpenIddictValidationProvider.cs
  87. 95
      src/OpenIddict.Validation/OpenIddictValidationTokenHandler.cs
  88. 55
      src/OpenIddict.Validation/OpenIddictValidationTransaction.cs
  89. 3
      src/OpenIddict/OpenIddict.csproj

42
OpenIddict.sln

@ -80,6 +80,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{F6F3C8E0-B
NuGet.config = NuGet.config NuGet.config = NuGet.config
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation", "src\OpenIddict.Validation\OpenIddict.Validation.csproj", "{17C10B53-278B-416F-9090-8531179BDF2E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.AspNetCore", "src\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj", "{08892053-5CE5-4153-B754-58D067C75028}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.DataProtection", "src\OpenIddict.Validation.DataProtection\OpenIddict.Validation.DataProtection.csproj", "{BD2463CF-7E1C-40AB-A33C-A44E5C9F23DF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.Owin", "src\OpenIddict.Validation.Owin\OpenIddict.Validation.Owin.csproj", "{B5F6A324-AB31-47EB-BF98-48C7105C4BCE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.ServerIntegration", "src\OpenIddict.Validation.ServerIntegration\OpenIddict.Validation.ServerIntegration.csproj", "{36FE030D-855F-4971-9E1A-76DACE53D349}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.SystemNetHttp", "src\OpenIddict.Validation.SystemNetHttp\OpenIddict.Validation.SystemNetHttp.csproj", "{AC3F3AFC-0E3A-4D3B-A245-58211AE630E5}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -190,6 +202,30 @@ Global
{C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Release|Any CPU.Build.0 = Release|Any CPU {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Release|Any CPU.Build.0 = Release|Any CPU
{17C10B53-278B-416F-9090-8531179BDF2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17C10B53-278B-416F-9090-8531179BDF2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17C10B53-278B-416F-9090-8531179BDF2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17C10B53-278B-416F-9090-8531179BDF2E}.Release|Any CPU.Build.0 = Release|Any CPU
{08892053-5CE5-4153-B754-58D067C75028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08892053-5CE5-4153-B754-58D067C75028}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08892053-5CE5-4153-B754-58D067C75028}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08892053-5CE5-4153-B754-58D067C75028}.Release|Any CPU.Build.0 = Release|Any CPU
{BD2463CF-7E1C-40AB-A33C-A44E5C9F23DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD2463CF-7E1C-40AB-A33C-A44E5C9F23DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD2463CF-7E1C-40AB-A33C-A44E5C9F23DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD2463CF-7E1C-40AB-A33C-A44E5C9F23DF}.Release|Any CPU.Build.0 = Release|Any CPU
{B5F6A324-AB31-47EB-BF98-48C7105C4BCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5F6A324-AB31-47EB-BF98-48C7105C4BCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5F6A324-AB31-47EB-BF98-48C7105C4BCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5F6A324-AB31-47EB-BF98-48C7105C4BCE}.Release|Any CPU.Build.0 = Release|Any CPU
{36FE030D-855F-4971-9E1A-76DACE53D349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36FE030D-855F-4971-9E1A-76DACE53D349}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36FE030D-855F-4971-9E1A-76DACE53D349}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36FE030D-855F-4971-9E1A-76DACE53D349}.Release|Any CPU.Build.0 = Release|Any CPU
{AC3F3AFC-0E3A-4D3B-A245-58211AE630E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC3F3AFC-0E3A-4D3B-A245-58211AE630E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC3F3AFC-0E3A-4D3B-A245-58211AE630E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC3F3AFC-0E3A-4D3B-A245-58211AE630E5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -221,6 +257,12 @@ Global
{97A59757-A249-4FCF-B042-BF425E117706} = {D544447C-D701-46BB-9A5B-C76C612A596B} {97A59757-A249-4FCF-B042-BF425E117706} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{1BD05607-C964-477C-A26A-73F01F7BB06E} = {D544447C-D701-46BB-9A5B-C76C612A596B} {1BD05607-C964-477C-A26A-73F01F7BB06E} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8} = {D544447C-D701-46BB-9A5B-C76C612A596B} {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{17C10B53-278B-416F-9090-8531179BDF2E} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{08892053-5CE5-4153-B754-58D067C75028} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{BD2463CF-7E1C-40AB-A33C-A44E5C9F23DF} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{B5F6A324-AB31-47EB-BF98-48C7105C4BCE} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{36FE030D-855F-4971-9E1A-76DACE53D349} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{AC3F3AFC-0E3A-4D3B-A245-58211AE630E5} = {D544447C-D701-46BB-9A5B-C76C612A596B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

6
samples/Mvc.Server/Controllers/ResourceController.cs

@ -1,7 +1,9 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Models; using Mvc.Server.Models;
using OpenIddict.Validation.AspNetCore;
namespace Mvc.Server.Controllers namespace Mvc.Server.Controllers
{ {
@ -15,14 +17,14 @@ namespace Mvc.Server.Controllers
_userManager = userManager; _userManager = userManager;
} }
//[Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("message")] [HttpGet("message")]
public async Task<IActionResult> GetMessage() public async Task<IActionResult> GetMessage()
{ {
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
if (user == null) if (user == null)
{ {
return BadRequest(); return Challenge(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
} }
return Content($"{user.UserName} has been successfully authenticated."); return Content($"{user.UserName} has been successfully authenticated.");

12
samples/Mvc.Server/Startup.cs

@ -109,6 +109,18 @@ namespace Mvc.Server
// options.IgnoreEndpointPermissions() // options.IgnoreEndpointPermissions()
// .IgnoreGrantTypePermissions() // .IgnoreGrantTypePermissions()
// .IgnoreScopePermissions(); // .IgnoreScopePermissions();
})
.AddValidation(options =>
{
// Configure the audience accepted by this resource server.
options.AddAudiences("resource_server");
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
}); });
services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<IEmailSender, AuthMessageSender>();

16
src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs

@ -295,6 +295,22 @@ namespace OpenIddict.Abstractions
return this; return this;
} }
/// <summary>
/// Tries to get the value corresponding to a given parameter.
/// </summary>
/// <param name="name">The parameter name.</param>
/// <param name="value">The parameter value.</param>
/// <returns><c>true</c> if the parameter could be found, <c>false</c> otherwise.</returns>
public bool TryGetParameter([NotNull] string name, out OpenIddictParameter value)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name));
}
return Parameters.TryGetValue(name, out value);
}
/// <summary> /// <summary>
/// Returns a <see cref="string"/> representation of the current instance that can be used in logs. /// Returns a <see cref="string"/> representation of the current instance that can be used in logs.
/// Note: sensitive parameters like client secrets are automatically removed for security reasons. /// Note: sensitive parameters like client secrets are automatically removed for security reasons.

25
src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs

@ -264,6 +264,31 @@ namespace OpenIddict.Abstractions
_ => Value.ToString() _ => Value.ToString()
}; };
/// <summary>
/// Tries to get the child item corresponding to the specified name.
/// </summary>
/// <param name="name">The name of the child item.</param>
/// <param name="value">An <see cref="OpenIddictParameter"/> instance containing the item value.</param>
/// <returns><c>true</c> if the parameter could be found, <c>false</c> otherwise.</returns>
public bool TryGetParameter([NotNull] string name, out OpenIddictParameter value)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name));
}
if (Value is JObject dictionary && dictionary.TryGetValue(name, out JToken token))
{
value = new OpenIddictParameter(token);
return true;
}
value = default;
return false;
}
/// <summary> /// <summary>
/// Determines whether two <see cref="OpenIddictParameter"/> instances are equal. /// Determines whether two <see cref="OpenIddictParameter"/> instances are equal.
/// </summary> /// </summary>

2
src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj

@ -15,6 +15,8 @@
<ProjectReference Include="..\OpenIddict\OpenIddict.csproj" /> <ProjectReference Include="..\OpenIddict\OpenIddict.csproj" />
<ProjectReference Include="..\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj" /> <ProjectReference Include="..\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj" />
<ProjectReference Include="..\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj" /> <ProjectReference Include="..\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.DataProtection\OpenIddict.Validation.DataProtection.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

1
src/OpenIddict.Owin/OpenIddict.Owin.csproj

@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenIddict\OpenIddict.csproj" /> <ProjectReference Include="..\OpenIddict\OpenIddict.csproj" />
<ProjectReference Include="..\OpenIddict.Server.Owin\OpenIddict.Server.Owin.csproj" /> <ProjectReference Include="..\OpenIddict.Server.Owin\OpenIddict.Server.Owin.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.Owin\OpenIddict.Validation.Owin.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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

@ -74,7 +74,7 @@ namespace OpenIddict.Server.AspNetCore
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {
@ -151,7 +151,7 @@ namespace OpenIddict.Server.AspNetCore
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {
@ -210,7 +210,7 @@ namespace OpenIddict.Server.AspNetCore
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {
@ -257,7 +257,7 @@ namespace OpenIddict.Server.AspNetCore
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {

25
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs

@ -30,5 +30,30 @@ namespace OpenIddict.Server.DataProtection
public const string TokenId = ".token_id"; public const string TokenId = ".token_id";
public const string TokenUsage = ".token_usage"; public const string TokenUsage = ".token_usage";
} }
public static class Purposes
{
public static class Features
{
public const string ReferenceTokens = "UseReferenceTokens";
}
public static class Formats
{
public const string AccessToken = "AccessTokenFormat";
public const string AuthorizationCode = "AuthorizationCodeFormat";
public const string RefreshToken = "RefreshTokenFormat";
}
public static class Handlers
{
public const string Server = "OpenIdConnectServerHandler";
}
public static class Schemes
{
public const string Server = "ASOS";
}
}
} }
} }

2
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs

@ -22,7 +22,7 @@ namespace OpenIddict.Server.DataProtection
{ {
public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProtectionFormatter public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProtectionFormatter
{ {
public ClaimsPrincipal ReadToken(BinaryReader reader) public ClaimsPrincipal ReadToken([NotNull] BinaryReader reader)
{ {
if (reader == null) if (reader == null)
{ {

8
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -18,14 +19,15 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters;
using static OpenIddict.Server.OpenIddictServerConstants;
using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlerFilters; using static OpenIddict.Server.OpenIddictServerHandlerFilters;
using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.OpenIddictServerHandlers;
namespace OpenIddict.Server.DataProtection namespace OpenIddict.Server.DataProtection
{ {
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictServerDataProtectionHandlers public static partial class OpenIddictServerDataProtectionHandlers
{ {
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
@ -81,7 +83,7 @@ namespace OpenIddict.Server.DataProtection
.AddFilter<RequireTokenStorageEnabled>() .AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireReferenceTokensEnabled>() .AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceDataProtectionToken>() .UseScopedHandler<ValidateReferenceDataProtectionToken>()
.SetOrder(ValidateReferenceToken.Descriptor.Order - 500) .SetOrder(ValidateReferenceToken.Descriptor.Order + 500)
.Build(); .Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
@ -247,7 +249,7 @@ namespace OpenIddict.Server.DataProtection
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensDisabled>() .AddFilter<RequireReferenceTokensDisabled>()
.UseSingletonHandler<ValidateSelfContainedDataProtectionToken>() .UseSingletonHandler<ValidateSelfContainedDataProtectionToken>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order - 500) .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 500)
.Build(); .Build();
/// <summary> /// <summary>

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

@ -72,7 +72,7 @@ namespace OpenIddict.Server.Owin
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {
@ -164,7 +164,7 @@ namespace OpenIddict.Server.Owin
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {
@ -216,7 +216,7 @@ namespace OpenIddict.Server.Owin
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {
@ -264,7 +264,7 @@ namespace OpenIddict.Server.Owin
else if (context.IsRejected) else if (context.IsRejected)
{ {
var notification = new ProcessErrorResponseContext(transaction) var notification = new ProcessErrorContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse
{ {

5
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -35,6 +35,11 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
if (options.SecurityTokenHandler == null)
{
throw new InvalidOperationException("The security token handler cannot be null.");
}
// Ensure at least one flow has been enabled. // Ensure at least one flow has been enabled.
if (options.GrantTypes.Count == 0) if (options.GrantTypes.Count == 0)
{ {

25
src/OpenIddict.Server/OpenIddictServerConstants.cs

@ -15,30 +15,5 @@ namespace OpenIddict.Server
public const string ValidatedPostLogoutRedirectUri = ".validated_post_logout_redirect_uri"; public const string ValidatedPostLogoutRedirectUri = ".validated_post_logout_redirect_uri";
public const string ValidatedRedirectUri = ".validated_redirect_uri"; public const string ValidatedRedirectUri = ".validated_redirect_uri";
} }
public static class Purposes
{
public static class Features
{
public const string ReferenceTokens = "UseReferenceTokens";
}
public static class Formats
{
public const string AccessToken = "AccessTokenFormat";
public const string AuthorizationCode = "AuthorizationCodeFormat";
public const string RefreshToken = "RefreshTokenFormat";
}
public static class Handlers
{
public const string Server = "OpenIdConnectServerHandler";
}
public static class Schemes
{
public const string Server = "ASOS";
}
}
} }
} }

6
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -283,12 +283,12 @@ namespace OpenIddict.Server
/// <summary> /// <summary>
/// Represents an event called when processing an errored response. /// Represents an event called when processing an errored response.
/// </summary> /// </summary>
public class ProcessErrorResponseContext : BaseRequestContext public class ProcessErrorContext : BaseRequestContext
{ {
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="ProcessErrorResponseContext"/> class. /// Creates a new instance of the <see cref="ProcessErrorContext"/> class.
/// </summary> /// </summary>
public ProcessErrorResponseContext([NotNull] OpenIddictServerTransaction transaction) public ProcessErrorContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction) : base(transaction)
{ {
} }

2
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -31,7 +31,7 @@ namespace OpenIddict.Server
ValidateAuthorizationRequest.Descriptor, ValidateAuthorizationRequest.Descriptor,
HandleAuthorizationRequest.Descriptor, HandleAuthorizationRequest.Descriptor,
ApplyAuthorizationResponse<ProcessChallengeContext>.Descriptor, ApplyAuthorizationResponse<ProcessChallengeContext>.Descriptor,
ApplyAuthorizationResponse<ProcessErrorResponseContext>.Descriptor, ApplyAuthorizationResponse<ProcessErrorContext>.Descriptor,
ApplyAuthorizationResponse<ProcessRequestContext>.Descriptor, ApplyAuthorizationResponse<ProcessRequestContext>.Descriptor,
ApplyAuthorizationResponse<ProcessSigninContext>.Descriptor, ApplyAuthorizationResponse<ProcessSigninContext>.Descriptor,

4
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -34,7 +34,7 @@ namespace OpenIddict.Server
ExtractConfigurationRequest.Descriptor, ExtractConfigurationRequest.Descriptor,
ValidateConfigurationRequest.Descriptor, ValidateConfigurationRequest.Descriptor,
HandleConfigurationRequest.Descriptor, HandleConfigurationRequest.Descriptor,
ApplyConfigurationResponse<ProcessErrorResponseContext>.Descriptor, ApplyConfigurationResponse<ProcessErrorContext>.Descriptor,
ApplyConfigurationResponse<ProcessRequestContext>.Descriptor, ApplyConfigurationResponse<ProcessRequestContext>.Descriptor,
/* /*
@ -58,7 +58,7 @@ namespace OpenIddict.Server
ExtractCryptographyRequest.Descriptor, ExtractCryptographyRequest.Descriptor,
ValidateCryptographyRequest.Descriptor, ValidateCryptographyRequest.Descriptor,
HandleCryptographyRequest.Descriptor, HandleCryptographyRequest.Descriptor,
ApplyCryptographyResponse<ProcessErrorResponseContext>.Descriptor, ApplyCryptographyResponse<ProcessErrorContext>.Descriptor,
ApplyCryptographyResponse<ProcessRequestContext>.Descriptor, ApplyCryptographyResponse<ProcessRequestContext>.Descriptor,
/* /*

2
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -34,7 +34,7 @@ namespace OpenIddict.Server
ValidateTokenRequest.Descriptor, ValidateTokenRequest.Descriptor,
HandleTokenRequest.Descriptor, HandleTokenRequest.Descriptor,
ApplyTokenResponse<ProcessChallengeContext>.Descriptor, ApplyTokenResponse<ProcessChallengeContext>.Descriptor,
ApplyTokenResponse<ProcessErrorResponseContext>.Descriptor, ApplyTokenResponse<ProcessErrorContext>.Descriptor,
ApplyTokenResponse<ProcessRequestContext>.Descriptor, ApplyTokenResponse<ProcessRequestContext>.Descriptor,
ApplyTokenResponse<ProcessSigninContext>.Descriptor, ApplyTokenResponse<ProcessSigninContext>.Descriptor,

2
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -35,7 +35,7 @@ namespace OpenIddict.Server
ExtractIntrospectionRequest.Descriptor, ExtractIntrospectionRequest.Descriptor,
ValidateIntrospectionRequest.Descriptor, ValidateIntrospectionRequest.Descriptor,
HandleIntrospectionRequest.Descriptor, HandleIntrospectionRequest.Descriptor,
ApplyIntrospectionResponse<ProcessErrorResponseContext>.Descriptor, ApplyIntrospectionResponse<ProcessErrorContext>.Descriptor,
ApplyIntrospectionResponse<ProcessRequestContext>.Descriptor, ApplyIntrospectionResponse<ProcessRequestContext>.Descriptor,
/* /*

2
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -30,7 +30,7 @@ namespace OpenIddict.Server
ExtractRevocationRequest.Descriptor, ExtractRevocationRequest.Descriptor,
ValidateRevocationRequest.Descriptor, ValidateRevocationRequest.Descriptor,
HandleRevocationRequest.Descriptor, HandleRevocationRequest.Descriptor,
ApplyRevocationResponse<ProcessErrorResponseContext>.Descriptor, ApplyRevocationResponse<ProcessErrorContext>.Descriptor,
ApplyRevocationResponse<ProcessRequestContext>.Descriptor, ApplyRevocationResponse<ProcessRequestContext>.Descriptor,
/* /*

2
src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

@ -30,7 +30,7 @@ namespace OpenIddict.Server
ExtractLogoutRequest.Descriptor, ExtractLogoutRequest.Descriptor,
ValidateLogoutRequest.Descriptor, ValidateLogoutRequest.Descriptor,
HandleLogoutRequest.Descriptor, HandleLogoutRequest.Descriptor,
ApplyLogoutResponse<ProcessErrorResponseContext>.Descriptor, ApplyLogoutResponse<ProcessErrorContext>.Descriptor,
ApplyLogoutResponse<ProcessRequestContext>.Descriptor, ApplyLogoutResponse<ProcessRequestContext>.Descriptor,
ApplyLogoutResponse<ProcessSignoutContext>.Descriptor, ApplyLogoutResponse<ProcessSignoutContext>.Descriptor,

2
src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs

@ -32,7 +32,7 @@ namespace OpenIddict.Server
ValidateUserinfoRequest.Descriptor, ValidateUserinfoRequest.Descriptor,
HandleUserinfoRequest.Descriptor, HandleUserinfoRequest.Descriptor,
ApplyUserinfoResponse<ProcessChallengeContext>.Descriptor, ApplyUserinfoResponse<ProcessChallengeContext>.Descriptor,
ApplyUserinfoResponse<ProcessErrorResponseContext>.Descriptor, ApplyUserinfoResponse<ProcessErrorContext>.Descriptor,
ApplyUserinfoResponse<ProcessRequestContext>.Descriptor, ApplyUserinfoResponse<ProcessRequestContext>.Descriptor,
/* /*

76
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -793,8 +793,7 @@ namespace OpenIddict.Server
var authorization = await _authorizationManager.FindByIdAsync(identifier); var authorization = await _authorizationManager.FindByIdAsync(identifier);
if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) if (authorization == null || !await _authorizationManager.IsValidAsync(authorization))
{ {
context.Logger.LogError("The authorization associated with token '{Identifier}' " + context.Logger.LogError("The authorization '{Identifier}' was no longer valid.", identifier);
"was no longer valid.", context.Principal.GetInternalTokenId());
context.Reject( context.Reject(
error: context.EndpointType switch error: context.EndpointType switch
@ -1406,8 +1405,9 @@ namespace OpenIddict.Server
// Actors identities are also filtered (delegation scenarios). // Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim => var principal = context.Principal.Clone(claim =>
{ {
// Never exclude the subject claim. // Never exclude the subject and authorization identifier claims.
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
} }
@ -1655,13 +1655,14 @@ namespace OpenIddict.Server
// Actors identities are also filtered (delegation scenarios). // Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim => var principal = context.Principal.Clone(claim =>
{ {
// Never exclude the subject claim. // Never exclude the subject and authorization identifier claims.
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
} }
// Always exclude private claims, whose values must generally be kept secret. // Always exclude private claims by default, whose values must generally be kept secret.
if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
@ -2056,16 +2057,17 @@ namespace OpenIddict.Server
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
} }
descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(new SecurityTokenDescriptor descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(
{ new SecurityTokenDescriptor
Claims = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, {
EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( Claims = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.AccessToken },
credentials => credentials.Key is SymmetricSecurityKey), EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault(
Issuer = context.Issuer?.AbsoluteUri, credentials => credentials.Key is SymmetricSecurityKey),
SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => Issuer = context.Issuer?.AbsoluteUri,
credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials =>
Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(),
}); Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity
});
await _tokenManager.CreateAsync(descriptor); await _tokenManager.CreateAsync(descriptor);
@ -2166,16 +2168,17 @@ namespace OpenIddict.Server
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
} }
descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(new SecurityTokenDescriptor descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(
{ new SecurityTokenDescriptor
Claims = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, {
EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( Claims = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode },
credentials => credentials.Key is SymmetricSecurityKey), EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault(
Issuer = context.Issuer?.AbsoluteUri, credentials => credentials.Key is SymmetricSecurityKey),
SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => Issuer = context.Issuer?.AbsoluteUri,
credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials =>
Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(),
}); Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity
});
await _tokenManager.CreateAsync(descriptor); await _tokenManager.CreateAsync(descriptor);
@ -2276,15 +2279,16 @@ namespace OpenIddict.Server
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
} }
descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(new SecurityTokenDescriptor descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(
{ new SecurityTokenDescriptor
Claims = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, {
EncryptingCredentials = context.Options.EncryptionCredentials[0], Claims = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken },
Issuer = context.Issuer?.AbsoluteUri, EncryptingCredentials = context.Options.EncryptionCredentials[0],
SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => Issuer = context.Issuer?.AbsoluteUri,
credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials =>
Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(),
}); Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity
});
await _tokenManager.CreateAsync(descriptor); await _tokenManager.CreateAsync(descriptor);

6
src/OpenIddict.Server/OpenIddictServerProvider.cs

@ -6,7 +6,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -85,8 +84,11 @@ namespace OpenIddict.Server
descriptors.AddRange(_options.CurrentValue.CustomHandlers); descriptors.AddRange(_options.CurrentValue.CustomHandlers);
descriptors.AddRange(_options.CurrentValue.DefaultHandlers); descriptors.AddRange(_options.CurrentValue.DefaultHandlers);
foreach (var descriptor in descriptors.OrderBy(descriptor => descriptor.Order)) descriptors.Sort((left, right) => left.Order.CompareTo(right.Order));
for (var index = 0; index < descriptors.Count; index++)
{ {
var descriptor = descriptors[index];
if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor))
{ {
continue; continue;

5
src/OpenIddict.Server/OpenIddictServerTokenHandler.cs

@ -81,6 +81,11 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(parameters)); throw new ArgumentNullException(nameof(parameters));
} }
if (parameters.PropertyBag == null)
{
throw new InvalidOperationException("The property bag cannot be null.");
}
if (!parameters.PropertyBag.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type)) if (!parameters.PropertyBag.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type))
{ {
throw new InvalidOperationException("The token usage cannot be null or empty."); throw new InvalidOperationException("The token usage cannot be null or empty.");

25
src/OpenIddict.Validation.AspNetCore/OpenIddict.Validation.AspNetCore.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>ASP.NET Core integration package for the OpenIddict validation services.</Description>
<PackageTags>$(PackageTags);validation;aspnetcore</PackageTags>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="$(JsonNetBsonVersion)" />
</ItemGroup>
</Project>

73
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs

@ -0,0 +1,73 @@
/*
* 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;
using System.ComponentModel;
using JetBrains.Annotations;
using OpenIddict.Validation.AspNetCore;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure
/// the OpenIddict validation ASP.NET Core integration.
/// </summary>
public class OpenIddictValidationAspNetCoreBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationAspNetCoreBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationAspNetCoreBuilder([NotNull] 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 validation ASP.NET Core 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="OpenIddictValidationAspNetCoreBuilder"/>.</returns>
public OpenIddictValidationAspNetCoreBuilder Configure([NotNull] Action<OpenIddictValidationAspNetCoreOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <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><c>true</c> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals([CanBeNull] 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();
}
}

98
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs

@ -0,0 +1,98 @@
/*
* 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;
using System.Diagnostics;
using System.Text;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Contains the methods required to ensure that the OpenIddict validation configuration is valid.
/// </summary>
public class OpenIddictValidationAspNetCoreConfiguration : IConfigureOptions<AuthenticationOptions>,
IConfigureNamedOptions<OpenIddictValidationOptions>,
IPostConfigureOptions<AuthenticationOptions>
{
/// <summary>
/// Registers the OpenIddict validation handler in the global authentication options.
/// </summary>
/// <param name="options">The options instance to initialize.</param>
public void Configure([NotNull] AuthenticationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// If a handler was already registered and the type doesn't correspond to the OpenIddict handler, throw an exception.
if (options.SchemeMap.TryGetValue(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, out var builder) &&
builder.HandlerType != typeof(OpenIddictValidationAspNetCoreHandler))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The OpenIddict ASP.NET Core validation handler cannot be registered as an authentication scheme.")
.Append("This may indicate that an instance of another handler was registered with the same scheme.")
.ToString());
}
options.AddScheme<OpenIddictValidationAspNetCoreHandler>(
OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, displayName: null);
}
public void Configure([NotNull] OpenIddictValidationOptions options)
=> Debug.Fail("This infrastructure method shouldn't be called");
public void Configure([CanBeNull] string name, [NotNull] OpenIddictValidationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict ASP.NET Core validation components.
foreach (var handler in OpenIddictValidationAspNetCoreHandlers.DefaultHandlers)
{
options.DefaultHandlers.Add(handler);
}
}
/// <summary>
/// Ensures that the authentication configuration is in a consistent and valid state.
/// </summary>
/// <param name="name">The authentication scheme associated with the handler instance.</param>
/// <param name="options">The options instance to initialize.</param>
public void PostConfigure([CanBeNull] string name, [NotNull] AuthenticationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
bool TryValidate(string scheme)
{
// If the scheme was not set or if it cannot be found in the map, return true.
if (string.IsNullOrEmpty(scheme) || !options.SchemeMap.TryGetValue(scheme, out var builder))
{
return true;
}
return builder.HandlerType != typeof(OpenIddictValidationAspNetCoreHandler);
}
if (!TryValidate(options.DefaultSignInScheme) || !TryValidate(options.DefaultSignOutScheme))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The OpenIddict ASP.NET Core validation cannot be used as the default sign-in/sign-out handler.")
.Append("Make sure that neither DefaultSignInScheme nor DefaultSignOutScheme ")
.Append("point to an instance of the OpenIddict ASP.NET Core validation handler.")
.ToString());
}
}
}
}

27
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs

@ -0,0 +1,27 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Exposes common constants used by the OpenIddict ASP.NET Core host.
/// </summary>
public static class OpenIddictValidationAspNetCoreConstants
{
public static class Cache
{
public const string AuthorizationRequest = "openiddict-authorization-request:";
public const string LogoutRequest = "openiddict-logout-request:";
}
public static class Properties
{
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
}
}
}

21
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreDefaults.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.
*/
using Microsoft.AspNetCore.Authentication;
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Exposes the default values used by the OpenIddict validation handler.
/// </summary>
public static class OpenIddictValidationAspNetCoreDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "OpenIddict.Validation.AspNetCore";
}
}

88
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreExtensions.cs

@ -0,0 +1,88 @@
/*
* 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;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Validation;
using OpenIddict.Validation.AspNetCore;
using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters;
using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict validation services.
/// </summary>
public static class OpenIddictValidationAspNetCoreExtensions
{
/// <summary>
/// Registers the OpenIddict validation services for ASP.NET Core 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="OpenIddictValidationAspNetCoreBuilder"/>.</returns>
public static OpenIddictValidationAspNetCoreBuilder UseAspNetCore([NotNull] this OpenIddictValidationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddAuthentication();
builder.Services.TryAddScoped<OpenIddictValidationAspNetCoreHandler>();
// Register the built-in event handlers used by the OpenIddict ASP.NET Core validation components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Register the built-in filters used by the default OpenIddict ASP.NET Core validation event handlers.
builder.Services.TryAddSingleton<RequireHttpRequest>();
// Register the option initializer used by the OpenIddict ASP.NET Core validation integration services.
// Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IConfigureOptions<AuthenticationOptions>, OpenIddictValidationAspNetCoreConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<AuthenticationOptions>, OpenIddictValidationAspNetCoreConfiguration>(),
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictValidationOptions>, OpenIddictValidationAspNetCoreConfiguration>()
});
return new OpenIddictValidationAspNetCoreBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict validation services for ASP.NET Core 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 validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationBuilder UseAspNetCore(
[NotNull] this OpenIddictValidationBuilder builder,
[NotNull] Action<OpenIddictValidationAspNetCoreBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseAspNetCore());
return builder;
}
}
}

20
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs

@ -0,0 +1,20 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Exposes the current validation transaction to the ASP.NET Core host.
/// </summary>
public class OpenIddictValidationAspNetCoreFeature
{
/// <summary>
/// Gets or sets the validation transaction that encapsulates all specific
/// information about an individual OpenID Connect validation request.
/// </summary>
public OpenIddictValidationTransaction Transaction { get; set; }
}
}

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

@ -0,0 +1,225 @@
/*
* 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;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties;
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Provides the logic necessary to extract, validate and handle OpenID Connect requests.
/// </summary>
public class OpenIddictValidationAspNetCoreHandler : AuthenticationHandler<OpenIddictValidationAspNetCoreOptions>,
IAuthenticationRequestHandler
{
private readonly IOpenIddictValidationProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationAspNetCoreHandler"/> class.
/// </summary>
public OpenIddictValidationAspNetCoreHandler(
[NotNull] IOpenIddictValidationProvider provider,
[NotNull] IOptionsMonitor<OpenIddictValidationAspNetCoreOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder,
[NotNull] ISystemClock clock)
: base(options, logger, encoder, clock)
=> _provider = provider;
public async Task<bool> HandleRequestAsync()
{
// Note: the transaction may be already attached when replaying an ASP.NET Core request
// (e.g when using the built-in status code pages middleware with the re-execute mode).
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
// Create a new transaction and attach the HTTP request to make it available to the ASP.NET Core handlers.
transaction = await _provider.CreateTransactionAsync();
transaction.Properties[typeof(HttpRequest).FullName] = new WeakReference<HttpRequest>(Request);
// Attach the OpenIddict validation transaction to the ASP.NET Core features
// so that it can retrieved while performing challenge/forbid operations.
Context.Features.Set(new OpenIddictValidationAspNetCoreFeature { Transaction = transaction });
}
var context = new ProcessRequestContext(transaction);
await _provider.DispatchAsync(context);
if (context.IsRequestHandled)
{
return true;
}
else if (context.IsRequestSkipped)
{
return false;
}
else if (context.IsRejected)
{
var notification = new ProcessErrorContext(transaction)
{
Response = new OpenIddictResponse
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri
}
};
await _provider.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
return true;
}
else if (notification.IsRequestSkipped)
{
return false;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The OpenID Connect response was not correctly processed. This may indicate ")
.Append("that the event handler responsible of processing OpenID Connect responses ")
.Append("was not registered or was explicitly removed from the handlers list.")
.ToString());
}
return false;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
if (context.Principal == null || context.IsRequestHandled || context.IsRequestSkipped)
{
return AuthenticateResult.NoResult();
}
else if (context.IsRejected)
{
var builder = new StringBuilder();
if (!string.IsNullOrEmpty(context.Error))
{
builder.AppendLine("An error occurred while authenticating the current request:");
builder.AppendFormat("Error code: ", context.Error);
if (!string.IsNullOrEmpty(context.ErrorDescription))
{
builder.AppendLine();
builder.AppendFormat("Error description: ", context.ErrorDescription);
}
if (!string.IsNullOrEmpty(context.ErrorUri))
{
builder.AppendLine();
builder.AppendFormat("Error URI: ", context.ErrorUri);
}
}
else
{
builder.Append("An unknown error occurred while authenticating the current request.");
}
return AuthenticateResult.Fail(new Exception(builder.ToString())
{
// Note: the error details are stored as additional exception properties,
// which is similar to what other ASP.NET Core security handlers do.
Data =
{
[Parameters.Error] = context.Error,
[Parameters.ErrorDescription] = context.ErrorDescription,
[Parameters.ErrorUri] = context.ErrorUri
}
});
}
return AuthenticateResult.Success(new AuthenticationTicket(
context.Principal,
OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme));
}
protected override async Task HandleChallengeAsync([CanBeNull] AuthenticationProperties properties)
{
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var context = new ProcessChallengeContext(transaction)
{
Response = new OpenIddictResponse
{
Error = GetProperty(properties, Properties.Error),
ErrorDescription = GetProperty(properties, Properties.ErrorDescription),
ErrorUri = GetProperty(properties, Properties.ErrorUri)
}
};
await _provider.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
else if (context.IsRejected)
{
var notification = new ProcessErrorContext(transaction)
{
Response = new OpenIddictResponse
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri
}
};
await _provider.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The OpenID Connect response was not correctly processed. This may indicate ")
.Append("that the event handler responsible of processing OpenID Connect responses ")
.Append("was not registered or was explicitly removed from the handlers list.")
.ToString());
}
static string GetProperty(AuthenticationProperties properties, string name)
=> properties != null && properties.Items.TryGetValue(name, out string value) ? value : null;
}
protected override Task HandleForbiddenAsync([CanBeNull] AuthenticationProperties properties)
=> HandleChallengeAsync(properties);
}
}

38
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlerFilters.cs

@ -0,0 +1,38 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Contains a collection of event handler filters commonly used by the ASP.NET Core handlers.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictValidationAspNetCoreHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if no ASP.NET Core request can be found.
/// </summary>
public class RequireHttpRequest : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Transaction.GetHttpRequest() != null);
}
}
}
}

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

@ -0,0 +1,303 @@
/*
* 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;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation.AspNetCore
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictValidationAspNetCoreHandlers
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Request top-level processing:
*/
InferIssuerFromHost.Descriptor,
ExtractGetOrPostRequest.Descriptor,
ExtractAccessToken.Descriptor,
/*
* Response processing:
*/
ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of infering the default issuer from the HTTP request host and validating it.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class InferIssuerFromHost : IOpenIddictValidationHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferIssuerFromHost>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest();
if (request == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
// Only use the current host as the issuer if the
// issuer was not explicitly set in the options.
if (context.Issuer != null)
{
return default;
}
if (!request.Host.HasValue)
{
context.Reject(
error: Errors.InvalidRequest,
description: "The mandatory 'Host' header is missing.");
return default;
}
if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer) ||
!issuer.IsWellFormedOriginalString())
{
context.Reject(
error: Errors.InvalidRequest,
description: "The specified 'Host' header is invalid.");
return default;
}
context.Issuer = issuer;
return default;
}
}
/// <summary>
/// Contains the logic responsible of extracting OpenID Connect requests from GET or POST HTTP requests.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ExtractGetOrPostRequest : IOpenIddictValidationHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractGetOrPostRequest>()
.SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ProcessRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest();
if (request == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
if (HttpMethods.IsGet(request.Method))
{
context.Request = new OpenIddictRequest(request.Query);
}
else if (HttpMethods.IsPost(request.Method) && !string.IsNullOrEmpty(request.ContentType) &&
request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
{
context.Request = new OpenIddictRequest(await request.ReadFormAsync(request.HttpContext.RequestAborted));
}
else
{
context.Request = new OpenIddictRequest();
}
}
}
/// <summary>
/// Contains the logic responsible of extracting an access token from the standard HTTP Authorization header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ExtractAccessToken : IOpenIddictValidationHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractAccessToken>()
.SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest();
if (request == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
string header = request.Headers[HeaderNames.Authorization];
if (string.IsNullOrEmpty(header) || !header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return default;
}
// Attach the access token to the request message.
context.Request.AccessToken = header.Substring("Bearer ".Length);
return default;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ProcessJsonResponse<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest();
if (request == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
context.Logger.LogInformation("The response was successfully returned as a JSON document: {Response}.", context.Response);
using (var buffer = new MemoryStream())
using (var writer = new JsonTextWriter(new StreamWriter(buffer)))
{
var serializer = JsonSerializer.CreateDefault();
serializer.Serialize(writer, context.Response);
writer.Flush();
if (!string.IsNullOrEmpty(context.Response.Error))
{
if (context.Issuer == null)
{
throw new InvalidOperationException("The issuer address cannot be inferred from the current request.");
}
request.HttpContext.Response.StatusCode = 401;
request.HttpContext.Response.Headers[HeaderNames.WWWAuthenticate] = new StringBuilder()
.Append(Schemes.Bearer)
.Append(' ')
.Append(Parameters.Realm)
.Append("=\"")
.Append(context.Issuer.AbsoluteUri)
.Append('"')
.ToString();
}
request.HttpContext.Response.ContentLength = buffer.Length;
request.HttpContext.Response.ContentType = "application/json;charset=UTF-8";
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(request.HttpContext.Response.Body, 4096, request.HttpContext.RequestAborted);
}
context.HandleRequest();
}
}
}
}

92
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs

@ -0,0 +1,92 @@
/*
* 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;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using OpenIddict.Abstractions;
using OpenIddict.Validation;
using OpenIddict.Validation.AspNetCore;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace Microsoft.AspNetCore
{
/// <summary>
/// Exposes companion extensions for the OpenIddict/ASP.NET Core integration.
/// </summary>
public static class OpenIddictValidationAspNetCoreHelpers
{
/// <summary>
/// Retrieves the <see cref="HttpRequest"/> instance stored in the <see cref="OpenIddictValidationTransaction"/> properties.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="HttpRequest"/> instance or <c>null</c> if it couldn't be found.</returns>
public static HttpRequest GetHttpRequest([NotNull] this OpenIddictValidationTransaction transaction)
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
if (!transaction.Properties.TryGetValue(typeof(HttpRequest).FullName, out object property))
{
return null;
}
if (property is WeakReference<HttpRequest> reference && reference.TryGetTarget(out HttpRequest request))
{
return request;
}
return null;
}
/// <summary>
/// Retrieves the <see cref="OpenIddictValidationEndpointType"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictValidationEndpointType"/>.</returns>
public static OpenIddictValidationEndpointType GetOpenIddictValidationEndpointType([NotNull] this HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction?.EndpointType ?? default;
}
/// <summary>
/// Retrieves the <see cref="OpenIddictRequest"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictRequest"/> instance or <c>null</c> if it couldn't be found.</returns>
public static OpenIddictRequest GetOpenIddictValidationRequest([NotNull] this HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction?.Request;
}
/// <summary>
/// Retrieves the <see cref="OpenIddictResponse"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictResponse"/> instance or <c>null</c> if it couldn't be found.</returns>
public static OpenIddictResponse GetOpenIddictValidationResponse([NotNull] this HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction?.Response;
}
}
}

17
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs

@ -0,0 +1,17 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using Microsoft.AspNetCore.Authentication;
namespace OpenIddict.Validation.AspNetCore
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict ASP.NET Core validation integration.
/// </summary>
public class OpenIddictValidationAspNetCoreOptions : AuthenticationSchemeOptions
{
}
}

17
src/OpenIddict.Validation.DataProtection/IOpenIddictValidationDataProtectionFormatter.cs

@ -0,0 +1,17 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.IO;
using System.Security.Claims;
using JetBrains.Annotations;
namespace OpenIddict.Validation.DataProtection
{
public interface IOpenIddictValidationDataProtectionFormatter
{
ClaimsPrincipal ReadToken([NotNull] BinaryReader reader);
}
}

21
src/OpenIddict.Validation.DataProtection/OpenIddict.Validation.DataProtection.csproj

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>ASP.NET Core Data Protection integration package for the OpenIddict validation services.</Description>
<PackageTags>$(PackageTags);validation;dataprotection</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="$(AspNetCoreVersion)" />
</ItemGroup>
</Project>

105
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionBuilder.cs

@ -0,0 +1,105 @@
/*
* 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;
using System.ComponentModel;
using JetBrains.Annotations;
using Microsoft.AspNetCore.DataProtection;
using OpenIddict.Validation.DataProtection;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure the
/// OpenIddict ASP.NET Core Data Protection integration.
/// </summary>
public class OpenIddictValidationDataProtectionBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationDataProtectionBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationDataProtectionBuilder([NotNull] 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 validation ASP.NET Core Data Protection 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="OpenIddictValidationDataProtectionBuilder"/>.</returns>
public OpenIddictValidationDataProtectionBuilder Configure([NotNull] Action<OpenIddictValidationDataProtectionOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Configures OpenIddict to use a specific data protection provider
/// instead of relying on the default instance provided by the DI container.
/// </summary>
/// <param name="provider">The data protection provider used to create token protectors.</param>
/// <returns>The <see cref="OpenIddictValidationDataProtectionBuilder"/>.</returns>
public OpenIddictValidationDataProtectionBuilder UseDataProtectionProvider([NotNull] IDataProtectionProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException(nameof(provider));
}
return Configure(options => options.DataProtectionProvider = provider);
}
/// <summary>
/// Configures OpenIddict to use a specific formatter instead of relying on the default instance.
/// </summary>
/// <param name="formatter">The formatter used to read and write tokens.</param>
/// <returns>The <see cref="OpenIddictValidationDataProtectionBuilder"/>.</returns>
public OpenIddictValidationDataProtectionBuilder UseFormatter([NotNull] IOpenIddictValidationDataProtectionFormatter formatter)
{
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
return Configure(options => options.Formatter = formatter);
}
/// <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><c>true</c> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals([CanBeNull] 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();
}
}

64
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs

@ -0,0 +1,64 @@
/*
* 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;
using JetBrains.Annotations;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Validation.DataProtection
{
/// <summary>
/// Contains the methods required to ensure that the OpenIddict ASP.NET Core Data Protection configuration is valid.
/// </summary>
public class OpenIddictValidationDataProtectionConfiguration : IConfigureOptions<OpenIddictValidationOptions>,
IPostConfigureOptions<OpenIddictValidationDataProtectionOptions>
{
private readonly IDataProtectionProvider _dataProtectionProvider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationDataProtectionConfiguration"/> class.
/// </summary>
/// <param name="dataProtectionProvider">The ASP.NET Core Data Protection provider.</param>
public OpenIddictValidationDataProtectionConfiguration([NotNull] IDataProtectionProvider dataProtectionProvider)
=> _dataProtectionProvider = dataProtectionProvider;
public void Configure([NotNull] OpenIddictValidationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Use empty token validation parameters to ensure the core OpenIddict validation components
// don't throw an exception stating that an issuer or a metadata address was not set.
options.TokenValidationParameters = new TokenValidationParameters();
// Register the built-in event handlers used by the OpenIddict Data Protection validation components.
foreach (var handler in OpenIddictValidationDataProtectionHandlers.DefaultHandlers)
{
options.DefaultHandlers.Add(handler);
}
}
/// <summary>
/// Populates the default OpenIddict ASP.NET Core Data Protection validation options
/// and ensures that the configuration is in a consistent and valid state.
/// </summary>
/// <param name="name">The authentication scheme associated with the handler instance.</param>
/// <param name="options">The options instance to initialize.</param>
public void PostConfigure([CanBeNull] string name, [NotNull] OpenIddictValidationDataProtectionOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.DataProtectionProvider ??= _dataProtectionProvider;
}
}
}

57
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs

@ -0,0 +1,57 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation.DataProtection
{
public static class OpenIddictValidationDataProtectionConstants
{
public static class Properties
{
public const string AccessTokenLifetime = ".access_token_lifetime";
public const string AuthorizationCodeLifetime = ".authorization_code_lifetime";
public const string Audiences = ".audiences";
public const string CodeChallenge = ".code_challenge";
public const string CodeChallengeMethod = ".code_challenge_method";
public const string DataProtector = ".data_protector";
public const string Expires = ".expires";
public const string IdentityTokenLifetime = ".identity_token_lifetime";
public const string InternalAuthorizationId = ".internal_authorization_id";
public const string InternalTokenId = ".internal_token_id";
public const string Issued = ".issued";
public const string Nonce = ".nonce";
public const string OriginalRedirectUri = ".original_redirect_uri";
public const string Presenters = ".presenters";
public const string RefreshTokenLifetime = ".refresh_token_lifetime";
public const string Resources = ".resources";
public const string Scopes = ".scopes";
public const string TokenId = ".token_id";
public const string TokenUsage = ".token_usage";
}
public static class Purposes
{
public static class Features
{
public const string ReferenceTokens = "UseReferenceTokens";
}
public static class Formats
{
public const string AccessToken = "AccessTokenFormat";
}
public static class Handlers
{
public const string Server = "OpenIdConnectServerHandler";
}
public static class Schemes
{
public const string Server = "ASOS";
}
}
}
}

78
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionExtensions.cs

@ -0,0 +1,78 @@
/*
* 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;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Validation;
using OpenIddict.Validation.DataProtection;
using static OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionHandlers;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict ASP.NET Core Data Protection validation services.
/// </summary>
public static class OpenIddictValidationDataProtectionExtensions
{
/// <summary>
/// Registers the OpenIddict ASP.NET Core Data Protection validation services 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="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationDataProtectionBuilder UseDataProtection([NotNull] this OpenIddictValidationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddDataProtection();
// Register the built-in validation event handlers used by the OpenIddict Data Protection components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictValidationOptions>, OpenIddictValidationDataProtectionConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictValidationDataProtectionOptions>, OpenIddictValidationDataProtectionConfiguration>()
});
return new OpenIddictValidationDataProtectionBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict ASP.NET Core Data Protection validation services 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 validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationBuilder UseDataProtection(
[NotNull] this OpenIddictValidationBuilder builder,
[NotNull] Action<OpenIddictValidationDataProtectionBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseDataProtection());
return builder;
}
}
}

183
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs

@ -0,0 +1,183 @@
/*
* 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using Properties = OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants.Properties;
namespace OpenIddict.Validation.DataProtection
{
public class OpenIddictValidationDataProtectionFormatter : IOpenIddictValidationDataProtectionFormatter
{
public ClaimsPrincipal ReadToken([NotNull] BinaryReader reader)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}
var (principal, properties) = Read(reader, version: 5);
if (principal == null)
{
return null;
}
// Tokens serialized using the ASP.NET Core Data Protection stack are compound
// of both claims and special authentication properties. To ensure existing tokens
// can be reused, well-known properties are manually mapped to their claims equivalents.
return principal
.SetAudiences(GetArrayProperty(properties, Properties.Audiences))
.SetCreationDate(GetDateProperty(properties, Properties.Issued))
.SetExpirationDate(GetDateProperty(properties, Properties.Expires))
.SetPresenters(GetArrayProperty(properties, Properties.Presenters))
.SetScopes(GetArrayProperty(properties, Properties.Scopes))
.SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime))
.SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime))
.SetClaim(Claims.Private.AuthorizationId, GetProperty(properties, Properties.InternalAuthorizationId))
.SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge))
.SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod))
.SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime))
.SetClaim(Claims.Private.Nonce, GetProperty(properties, Properties.Nonce))
.SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri))
.SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime))
.SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId));
static (ClaimsPrincipal principal, ImmutableDictionary<string, string> properties) Read(BinaryReader reader, int version)
{
if (version != reader.ReadInt32())
{
return (null, ImmutableDictionary.Create<string, string>());
}
// Read the authentication scheme associated to the ticket.
_ = reader.ReadString();
// Read the number of identities stored in the serialized payload.
var count = reader.ReadInt32();
if (count < 0)
{
return (null, ImmutableDictionary.Create<string, string>());
}
var identities = new ClaimsIdentity[count];
for (var index = 0; index != count; ++index)
{
identities[index] = ReadIdentity(reader);
}
var properties = ReadProperties(reader, version);
return (new ClaimsPrincipal(identities), properties);
}
static ClaimsIdentity ReadIdentity(BinaryReader reader)
{
var identity = new ClaimsIdentity(
authenticationType: reader.ReadString(),
nameType: ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType),
roleType: ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType));
// Read the number of claims contained in the serialized identity.
var count = reader.ReadInt32();
for (int index = 0; index != count; ++index)
{
var claim = ReadClaim(reader, identity);
identity.AddClaim(claim);
}
// Determine whether the identity has a bootstrap context attached.
if (reader.ReadBoolean())
{
identity.BootstrapContext = reader.ReadString();
}
// Determine whether the identity has an actor identity attached.
if (reader.ReadBoolean())
{
identity.Actor = ReadIdentity(reader);
}
return identity;
}
static Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity)
{
var type = ReadWithDefault(reader, identity.NameClaimType);
var value = reader.ReadString();
var valueType = ReadWithDefault(reader, ClaimValueTypes.String);
var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer);
var originalIssuer = ReadWithDefault(reader, issuer);
var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity);
// Read the number of properties stored in the claim.
var count = reader.ReadInt32();
for (var index = 0; index != count; ++index)
{
var key = reader.ReadString();
var propertyValue = reader.ReadString();
claim.Properties.Add(key, propertyValue);
}
return claim;
}
static ImmutableDictionary<string, string> ReadProperties(BinaryReader reader, int version)
{
if (version != reader.ReadInt32())
{
return ImmutableDictionary.Create<string, string>();
}
var properties = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
var count = reader.ReadInt32();
for (var index = 0; index != count; ++index)
{
properties.Add(reader.ReadString(), reader.ReadString());
}
return properties.ToImmutable();
}
static string ReadWithDefault(BinaryReader reader, string defaultValue)
{
var value = reader.ReadString();
if (string.Equals(value, "\0", StringComparison.Ordinal))
{
return defaultValue;
}
return value;
}
static string GetProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? value : null;
static IEnumerable<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values<string>() : Enumerable.Empty<string>();
static DateTimeOffset? GetDateProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? (DateTimeOffset?)
DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null;
}
}
}

232
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs

@ -0,0 +1,232 @@
/*
* 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;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.OpenIddictValidationHandlerFilters;
using static OpenIddict.Validation.OpenIddictValidationHandlers;
namespace OpenIddict.Validation.DataProtection
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictValidationDataProtectionHandlers
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authentication processing:
*/
ValidateReferenceDataProtectionToken.Descriptor,
ValidateSelfContainedDataProtectionToken.Descriptor);
/// <summary>
/// Contains the logic responsible of rejecting authentication
/// demands that use an invalid reference Data Protection token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class ValidateReferenceDataProtectionToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictValidationDataProtectionOptions> _options;
public ValidateReferenceDataProtectionToken() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.Append("Alternatively, you can disable the built-in database-based server features by enabling ")
.Append("the degraded mode with 'services.AddOpenIddict().AddValidation().EnableDegradedMode()'.")
.ToString());
public ValidateReferenceDataProtectionToken(
[NotNull] IOpenIddictTokenManager tokenManager,
[NotNull] IOptionsMonitor<OpenIddictValidationDataProtectionOptions> options)
{
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceDataProtectionToken>()
.SetOrder(ValidateReferenceToken.Descriptor.Order + 500)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
{
return;
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var identifier = context.Request.AccessToken;
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByReferenceIdAsync(identifier);
if (token == null || !string.Equals(await _tokenManager.GetTypeAsync(token),
TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase))
{
return;
}
var payload = await _tokenManager.GetPayloadAsync(token);
if (string.IsNullOrEmpty(payload))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The payload associated with a reference token cannot be retrieved.")
.Append("This may indicate that the token entry was corrupted.")
.ToString());
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AccessToken,
Purposes.Features.ReferenceTokens,
Purposes.Schemes.Server);
ClaimsPrincipal principal = null;
try
{
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(payload)));
using var reader = new BinaryReader(buffer);
principal = _options.CurrentValue.Formatter.ReadToken(reader);
}
catch (Exception exception)
{
context.Logger.LogTrace(exception, "An exception occured while deserializing a token.");
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (principal == null)
{
return;
}
// Attach the principal extracted from the authorization code to the parent event context
// and restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal = principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetInternalTokenId(await _tokenManager.GetIdAsync(token))
.SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token));
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands
/// that specify an invalid self-contained Data Protection token.
/// </summary>
public class ValidateSelfContainedDataProtectionToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictValidationDataProtectionOptions> _options;
public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictValidationDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.UseSingletonHandler<ValidateSelfContainedDataProtectionToken>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order + 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
{
return default;
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var token = context.Request.AccessToken;
if (string.IsNullOrEmpty(token))
{
return default;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AccessToken,
Purposes.Schemes.Server);
ClaimsPrincipal principal = null;
try
{
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token)));
using var reader = new BinaryReader(buffer);
principal = _options.CurrentValue.Formatter.ReadToken(reader);
}
catch (Exception exception)
{
context.Logger.LogTrace(exception, "An exception occured while deserializing a token.");
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (principal == null)
{
return default;
}
// Note: since the data format relies on a data protector using different "purposes" strings
// per token type, the token processed at this stage is guaranteed to be of the expected type.
context.Principal = principal.SetClaim(Claims.Private.TokenUsage, TokenUsages.AccessToken);
return default;
}
}
}
}

31
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionOptions.cs

@ -0,0 +1,31 @@
/*
* 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.AspNetCore.DataProtection;
namespace OpenIddict.Validation.DataProtection
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict validation handler.
/// </summary>
public class OpenIddictValidationDataProtectionOptions
{
/// <summary>
/// Gets or sets the data protection provider used to create the default
/// data protectors used by the OpenIddict Data Protection validation services.
/// When this property is set to <c>null</c>, the data protection provider
/// is directly retrieved from the dependency injection container.
/// </summary>
public IDataProtectionProvider DataProtectionProvider { get; set; }
/// <summary>
/// Gets or sets the formatter used to read and write Data Protection tokens,
/// serialized using the same format as the ASP.NET Core authentication tickets.
/// </summary>
public IOpenIddictValidationDataProtectionFormatter Formatter { get; set; }
= new OpenIddictValidationDataProtectionFormatter();
}
}

24
src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>OWIN/ASP.NET 4.x integration package for the OpenIddict validation services.</Description>
<PackageTags>$(PackageTags);validation;aspnet;katana;owin</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Owin.Security" Version="$(OwinVersion)" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="$(JsonNetBsonVersion)" />
</ItemGroup>
</Project>

73
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs

@ -0,0 +1,73 @@
/*
* 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;
using System.ComponentModel;
using JetBrains.Annotations;
using OpenIddict.Validation.Owin;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure
/// the OpenIddict validation OWIN/Katana integration.
/// </summary>
public class OpenIddictValidationOwinBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationOwinBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationOwinBuilder([NotNull] 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 validation OWIN/Katana 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="OpenIddictValidationOwinBuilder"/>.</returns>
public OpenIddictValidationOwinBuilder Configure([NotNull] Action<OpenIddictValidationOwinOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <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><c>true</c> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals([CanBeNull] 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();
}
}

36
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConfiguration.cs

@ -0,0 +1,36 @@
/*
* 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;
using System.Diagnostics;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Contains the methods required to ensure that the OpenIddict validation configuration is valid.
/// </summary>
public class OpenIddictValidationOwinConfiguration : IConfigureNamedOptions<OpenIddictValidationOptions>
{
public void Configure([NotNull] OpenIddictValidationOptions options)
=> Debug.Fail("This infrastructure method shouldn't be called");
public void Configure([CanBeNull] string name, [NotNull] OpenIddictValidationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict OWIN validation components.
foreach (var handler in OpenIddictValidationOwinHandlers.DefaultHandlers)
{
options.DefaultHandlers.Add(handler);
}
}
}
}

27
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs

@ -0,0 +1,27 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Exposes common constants used by the OpenIddict OWIN host.
/// </summary>
public static class OpenIddictValidationOwinConstants
{
public static class Cache
{
public const string AuthorizationRequest = "openiddict-authorization-request:";
public const string LogoutRequest = "openiddict-logout-request:";
}
public static class Properties
{
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
}
}
}

21
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinDefaults.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.
*/
using Microsoft.Owin.Security;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Exposes the default values used by the OpenIddict validation handler.
/// </summary>
public static class OpenIddictValidationOwinDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationOptions.AuthenticationType"/>.
/// </summary>
public const string AuthenticationType = "OpenIddict.Validation.Owin";
}
}

87
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs

@ -0,0 +1,87 @@
/*
* 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;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Validation;
using OpenIddict.Validation.Owin;
using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlerFilters;
using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlers;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict validation services.
/// </summary>
public static class OpenIddictValidationOwinExtensions
{
/// <summary>
/// Registers the OpenIddict validation services for OWIN 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="OpenIddictValidationOwinBuilder"/>.</returns>
public static OpenIddictValidationOwinBuilder UseOwin([NotNull] this OpenIddictValidationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddWebEncoders();
// Note: unlike regular OWIN middleware, the OpenIddict validation middleware is registered
// as a scoped service in the DI container. This allows containers that support middleware
// resolution (like Autofac) to use it without requiring additional configuration.
builder.Services.TryAddScoped<OpenIddictValidationOwinMiddleware>();
// Register the built-in event handlers used by the OpenIddict OWIN validation components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Register the built-in filters used by the default OpenIddict OWIN validation event handlers.
builder.Services.TryAddSingleton<RequireOwinRequest>();
// Register the option initializers used by the OpenIddict OWIN validation integration services.
// Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictValidationOptions>, OpenIddictValidationOwinConfiguration>()
});
return new OpenIddictValidationOwinBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict validation services for OWIN 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 validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationBuilder UseOwin(
[NotNull] this OpenIddictValidationBuilder builder,
[NotNull] Action<OpenIddictValidationOwinBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseOwin());
return builder;
}
}
}

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

@ -0,0 +1,214 @@
/*
* 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;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Provides the entry point necessary to register the OpenIddict validation in an OWIN pipeline.
/// </summary>
public class OpenIddictValidationOwinHandler : AuthenticationHandler<OpenIddictValidationOwinOptions>
{
private readonly ILogger _logger;
private readonly IOpenIddictValidationProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinHandler"/> class.
/// </summary>
/// <param name="logger">The logger used by this instance.</param>
/// <param name="provider">The OpenIddict validation OWIN provider used by this instance.</param>
public OpenIddictValidationOwinHandler(
[NotNull] ILogger logger,
[NotNull] IOpenIddictValidationProvider provider)
{
_logger = logger;
_provider = provider;
}
public override async Task<bool> InvokeAsync()
{
// Note: the transaction may be already attached when replaying an OWIN request
// (e.g when using a status code pages middleware re-invoking the OWIN pipeline).
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
if (transaction == null)
{
// Create a new transaction and attach the OWIN request to make it available to the OWIN handlers.
transaction = await _provider.CreateTransactionAsync();
transaction.Properties[typeof(IOwinRequest).FullName] = new WeakReference<IOwinRequest>(Request);
// Attach the OpenIddict validation transaction to the OWIN shared dictionary
// so that it can retrieved while performing sign-in/sign-out operations.
Context.Set(typeof(OpenIddictValidationTransaction).FullName, transaction);
}
var context = new ProcessRequestContext(transaction);
await _provider.DispatchAsync(context);
if (context.IsRequestHandled)
{
return true;
}
else if (context.IsRequestSkipped)
{
return false;
}
else if (context.IsRejected)
{
var notification = new ProcessErrorContext(transaction)
{
Response = new OpenIddictResponse
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri
}
};
await _provider.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
return true;
}
else if (notification.IsRequestSkipped)
{
return false;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The OpenID Connect response was not correctly processed. This may indicate ")
.Append("that the event handler responsible of processing OpenID Connect responses ")
.Append("was not registered or was explicitly removed from the handlers list.")
.ToString());
}
return false;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
if (transaction?.Request == null)
{
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
if (context.Principal == null || context.IsRequestHandled || context.IsRequestSkipped)
{
return null;
}
else if (context.IsRejected)
{
_logger.LogError("An error occurred while authenticating the current request: {Error} ; {Description}",
/* Error: */ context.Error ?? Errors.InvalidToken,
/* Description: */ context.ErrorDescription);
return new AuthenticationTicket(identity: null, new AuthenticationProperties
{
Dictionary =
{
[Parameters.Error] = context.Error,
[Parameters.ErrorDescription] = context.ErrorDescription,
[Parameters.ErrorUri] = context.ErrorUri
}
});
}
return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, new AuthenticationProperties());
}
protected override async Task TeardownCoreAsync()
{
// Note: OWIN authentication handlers cannot reliabily write to the response stream
// from ApplyResponseGrantAsync or ApplyResponseChallengeAsync because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback,
// where calling Write or WriteAsync on the response stream may result in a deadlock
// on hosts using streamed responses. To work around this limitation, this handler
// doesn't implement ApplyResponseGrantAsync but TeardownCoreAsync, which is never called
// by AuthenticationHandler.OnSendingHeaderCallback. In theory, this would prevent
// OpenIddictValidationOwinMiddleware from both applying the response grant and allowing
// the next middleware in the pipeline to alter the response stream but in practice,
// OpenIddictValidationOwinMiddleware is assumed to be the only middleware allowed to write
// to the response stream when a response grant (sign-in/out or challenge) was applied.
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var context = new ProcessChallengeContext(transaction)
{
Response = new OpenIddictResponse
{
Error = GetProperty(challenge.Properties, Properties.Error),
ErrorDescription = GetProperty(challenge.Properties, Properties.ErrorDescription),
ErrorUri = GetProperty(challenge.Properties, Properties.ErrorUri)
}
};
await _provider.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
else if (context.IsRejected)
{
var notification = new ProcessErrorContext(transaction)
{
Response = new OpenIddictResponse
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri
}
};
await _provider.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The OpenID Connect response was not correctly processed. This may indicate ")
.Append("that the event handler responsible of processing OpenID Connect responses ")
.Append("was not registered or was explicitly removed from the handlers list.")
.ToString());
}
static string GetProperty(AuthenticationProperties properties, string name)
=> properties != null && properties.Dictionary.TryGetValue(name, out string value) ? value : null;
}
}
}
}

36
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlerFilters.cs

@ -0,0 +1,36 @@
/*
* 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;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Owin;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Contains a collection of event handler filters commonly used by the OWIN handlers.
/// </summary>
public static class OpenIddictValidationOwinHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if no OWIN request can be found.
/// </summary>
public class RequireOwinRequest : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Transaction.GetOwinRequest() != null);
}
}
}
}

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

@ -0,0 +1,302 @@
/*
* 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;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using OpenIddict.Abstractions;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlerFilters;
namespace OpenIddict.Validation.Owin
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictValidationOwinHandlers
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Request top-level processing:
*/
InferIssuerFromHost.Descriptor,
ExtractGetOrPostRequest.Descriptor,
ExtractAccessToken.Descriptor,
/*
* Response processing:
*/
ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of infering the default issuer from the HTTP request host and validating it.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class InferIssuerFromHost : IOpenIddictValidationHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<InferIssuerFromHost>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetOwinRequest();
if (request == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
// Only use the current host as the issuer if the
// issuer was not explicitly set in the options.
if (context.Issuer != null)
{
return default;
}
if (string.IsNullOrEmpty(request.Host.Value))
{
context.Reject(
error: Errors.InvalidRequest,
description: "The mandatory 'Host' header is missing.");
return default;
}
if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer) ||
!issuer.IsWellFormedOriginalString())
{
context.Reject(
error: Errors.InvalidRequest,
description: "The specified 'Host' header is invalid.");
return default;
}
context.Issuer = issuer;
return default;
}
}
/// <summary>
/// Contains the logic responsible of extracting OpenID Connect requests from GET or POST HTTP requests.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ExtractGetOrPostRequest : IOpenIddictValidationHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractGetOrPostRequest>()
.SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ProcessRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetOwinRequest();
if (request == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
if (string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
context.Request = new OpenIddictRequest(request.Query);
}
else if (string.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(request.ContentType) &&
request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
{
context.Request = new OpenIddictRequest(await request.ReadFormAsync());
}
else
{
context.Request = new OpenIddictRequest();
}
}
}
/// <summary>
/// Contains the logic responsible of extracting an access token from the standard HTTP Authorization header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ExtractAccessToken : IOpenIddictValidationHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractAccessToken>()
.SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetOwinRequest();
if (request == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
string header = request.Headers["Authorization"];
if (string.IsNullOrEmpty(header) || !header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return default;
}
// Attach the access token to the request message.
context.Request.AccessToken = header.Substring("Bearer ".Length);
return default;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ProcessJsonResponse<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetOwinRequest();
if (request == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
context.Logger.LogInformation("The response was successfully returned as a JSON document: {Response}.", context.Response);
using (var buffer = new MemoryStream())
using (var writer = new JsonTextWriter(new StreamWriter(buffer)))
{
var serializer = JsonSerializer.CreateDefault();
serializer.Serialize(writer, context.Response);
writer.Flush();
if (!string.IsNullOrEmpty(context.Response.Error))
{
if (context.Issuer == null)
{
throw new InvalidOperationException("The issuer address cannot be inferred from the current request.");
}
request.Context.Response.StatusCode = 401;
request.Context.Response.Headers["WWW-Authenticate"] = new StringBuilder()
.Append(Schemes.Bearer)
.Append(' ')
.Append(Parameters.Realm)
.Append("=\"")
.Append(context.Issuer.AbsoluteUri)
.Append('"')
.ToString();
}
request.Context.Response.ContentLength = buffer.Length;
request.Context.Response.ContentType = "application/json;charset=UTF-8";
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(request.Context.Response.Body, 4096, request.CallCancelled);
}
context.HandleRequest();
}
}
}
}

109
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHelpers.cs

@ -0,0 +1,109 @@
/*
* 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;
using JetBrains.Annotations;
using Microsoft.Owin;
using OpenIddict.Abstractions;
using OpenIddict.Validation;
using OpenIddict.Validation.Owin;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace Owin
{
/// <summary>
/// Exposes companion extensions for the OpenIddict/OWIN integration.
/// </summary>
public static class OpenIddictValidationOwinHelpers
{
/// <summary>
/// Registers the OpenIddict validation OWIN middleware in the application pipeline.
/// Note: when using a dependency injection container supporting per-request
/// middleware resolution (like Autofac), calling this method is NOT recommended.
/// </summary>
/// <param name="app">The application builder used to register middleware instances.</param>
/// <returns>The <see cref="IAppBuilder"/>.</returns>
public static IAppBuilder UseOpenIddictValidation([NotNull] this IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.Use<OpenIddictValidationOwinMiddlewareFactory>();
}
/// <summary>
/// Retrieves the <see cref="IOwinRequest"/> instance stored in the <see cref="OpenIddictValidationTransaction"/> properties.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="IOwinRequest"/> instance or <c>null</c> if it couldn't be found.</returns>
public static IOwinRequest GetOwinRequest([NotNull] this OpenIddictValidationTransaction transaction)
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
if (!transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName, out object property))
{
return null;
}
if (property is WeakReference<IOwinRequest> reference && reference.TryGetTarget(out IOwinRequest request))
{
return request;
}
return null;
}
/// <summary>
/// Retrieves the <see cref="OpenIddictValidationEndpointType"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictValidationEndpointType"/>.</returns>
public static OpenIddictValidationEndpointType GetOpenIddictValidationEndpointType([NotNull] this IOwinContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName)?.EndpointType ?? default;
}
/// <summary>
/// Retrieves the <see cref="OpenIddictRequest"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictRequest"/> instance or <c>null</c> if it couldn't be found.</returns>
public static OpenIddictRequest GetOpenIddictValidationRequest([NotNull] this IOwinContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName)?.Request;
}
/// <summary>
/// Retrieves the <see cref="OpenIddictResponse"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictResponse"/> instance or <c>null</c> if it couldn't be found.</returns>
public static OpenIddictResponse GetOpenIddictValidationResponse([NotNull] this IOwinContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName)?.Response;
}
}
}

51
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs

@ -0,0 +1,51 @@
/*
* 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 JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Owin;
using Microsoft.Owin.Security.Infrastructure;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Provides the entry point necessary to register the OpenIddict validation in an OWIN pipeline.
/// Note: this middleware is intented to be used with dependency injection containers
/// that support middleware resolution, like Autofac. Since it depends on scoped services,
/// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware.
/// </summary>
public class OpenIddictValidationOwinMiddleware : AuthenticationMiddleware<OpenIddictValidationOwinOptions>
{
private readonly ILogger<OpenIddictValidationOwinMiddleware> _logger;
private readonly IOpenIddictValidationProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline, if applicable.</param>
/// <param name="logger">The logger used by this middleware.</param>
/// <param name="options">The OpenIddict validation OWIN options.</param>
/// <param name="provider">The OpenIddict validation provider.</param>
public OpenIddictValidationOwinMiddleware(
[CanBeNull] OwinMiddleware next,
[NotNull] ILogger<OpenIddictValidationOwinMiddleware> logger,
[NotNull] IOptionsMonitor<OpenIddictValidationOwinOptions> options,
[NotNull] IOpenIddictValidationProvider provider)
: base(next, options.CurrentValue)
{
_logger = logger;
_provider = provider;
}
/// <summary>
/// Creates and returns a new <see cref="OpenIddictValidationOwinHandler"/> instance.
/// </summary>
/// <returns>A new instance of the <see cref="OpenIddictValidationOwinHandler"/> class.</returns>
protected override AuthenticationHandler<OpenIddictValidationOwinOptions> CreateHandler()
=> new OpenIddictValidationOwinHandler(_logger, _provider);
}
}

80
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs

@ -0,0 +1,80 @@
/*
* 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;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Owin;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Provides the entry point necessary to instantiate and register the scoped
/// <see cref="OpenIddictValidationOwinMiddleware"/> in an OWIN/Katana pipeline.
/// </summary>
public class OpenIddictValidationOwinMiddlewareFactory : OwinMiddleware
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinMiddlewareFactory"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline, if applicable.</param>
public OpenIddictValidationOwinMiddlewareFactory([CanBeNull] OwinMiddleware next)
: base(next)
{
}
/// <summary>
/// Resolves the <see cref="IServiceProvider"/> instance from the OWIN context
/// and creates a new instance of the <see cref="OpenIddictValidationOwinMiddleware"/> class,
/// which is used to register <see cref="OpenIddictValidationOwinHandler"/> in the pipeline.
/// </summary>
/// <param name="context">The <see cref="IOwinContext"/>.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override Task Invoke([NotNull] IOwinContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var provider = context.Get<IServiceProvider>(typeof(IServiceProvider).FullName);
if (provider == null)
{
throw new InvalidOperationException(new StringBuilder()
.Append("No service provider was found in the OWIN context. For the OpenIddict validation ")
.Append("services to work correctly, a per-request 'IServiceProvider' must be attached ")
.AppendLine("to the OWIN environment with the dictionary key 'System.IServiceProvider'.")
.Append("Note: when using a dependency injection container supporting middleware resolution ")
.Append("(like Autofac), the 'app.UseOpenIddictValidation()' extension MUST NOT be called.")
.ToString());
}
// Note: the Microsoft.Extensions.DependencyInjection container doesn't support resolving services
// with arbitrary parameters, which prevents the validation OWIN middleware from being resolved directly
// from the DI container, as the next middleware in the pipeline cannot be specified as a parameter.
// To work around this limitation, the validation OWIN middleware is manually instantiated and invoked.
var middleware = new OpenIddictValidationOwinMiddleware(
next: Next,
logger: GetRequiredService<ILogger<OpenIddictValidationOwinMiddleware>>(provider),
options: GetRequiredService<IOptionsMonitor<OpenIddictValidationOwinOptions>>(provider),
provider: GetRequiredService<IOpenIddictValidationProvider>(provider));
return middleware.Invoke(context);
static T GetRequiredService<T>(IServiceProvider provider)
=> provider.GetService<T>() ?? throw new InvalidOperationException(new StringBuilder()
.AppendLine("The OpenIddict validation authentication services cannot be resolved from the DI container.")
.Append("To register the OWIN services, use 'services.AddOpenIddict().AddValidation().UseOwin()'.")
.ToString());
}
}
}

23
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs

@ -0,0 +1,23 @@
/*
* 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.Owin.Security;
namespace OpenIddict.Validation.Owin
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict OWIN validation integration.
/// </summary>
public class OpenIddictValidationOwinOptions : AuthenticationOptions
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinOptions"/> class.
/// </summary>
public OpenIddictValidationOwinOptions()
: base(OpenIddictValidationOwinDefaults.AuthenticationType)
=> AuthenticationMode = AuthenticationMode.Passive;
}
}

21
src/OpenIddict.Validation.ServerIntegration/OpenIddict.Validation.ServerIntegration.csproj

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>OpenID Connect validation/server integration for OpenIddict.</Description>
<PackageTags>$(PackageTags);server;validation</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" />
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
</ItemGroup>
</Project>

72
src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationBuilder.cs

@ -0,0 +1,72 @@
/*
* 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;
using System.ComponentModel;
using JetBrains.Annotations;
using OpenIddict.Validation.ServerIntegration;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict validation services.
/// </summary>
public class OpenIddictValidationServerIntegrationBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationServerIntegrationBuilder([NotNull] 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 validation/server integration 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="OpenIddictValidationServerIntegrationBuilder"/>.</returns>
public OpenIddictValidationServerIntegrationBuilder Configure([NotNull] Action<OpenIddictValidationServerIntegrationOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <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><c>true</c> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals([CanBeNull] 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();
}
}

65
src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs

@ -0,0 +1,65 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
namespace OpenIddict.Validation.ServerIntegration
{
/// <summary>
/// Contains the methods required to ensure that the OpenIddict validation/server integration configuration is valid.
/// </summary>
public class OpenIddictValidationServerIntegrationConfiguration : IConfigureOptions<OpenIddictValidationOptions>
{
private readonly IOptionsMonitor<OpenIddictServerOptions> _options;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationServerIntegrationConfiguration"/> class.
/// </summary>
/// <param name="options">The OpenIddict server options.</param>
public OpenIddictValidationServerIntegrationConfiguration([NotNull] IOptionsMonitor<OpenIddictServerOptions> options)
=> _options = options;
/// <summary>
/// Populates the default OpenIddict validation/server integration options
/// and ensures that the configuration is in a consistent and valid state.
/// </summary>
/// <param name="options">The options instance to initialize.</param>
public void Configure([NotNull] OpenIddictValidationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Note: the issuer may be null. In this case, it will be usually be provided by
// a validation handler registered by the host (e.g ASP.NET Core or OWIN/Katana)
options.Issuer = _options.CurrentValue.Issuer;
// Import the token validation parameters from the server configuration.
options.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKeys = (from credentials in _options.CurrentValue.SigningCredentials
select credentials.Key).ToList()
};
// Import the symmetric encryption keys from the server configuration.
foreach (var credentials in _options.CurrentValue.EncryptionCredentials)
{
if (credentials.Key is SymmetricSecurityKey)
{
options.EncryptionCredentials.Add(credentials);
}
}
options.UseReferenceTokens = _options.CurrentValue.UseReferenceTokens;
}
}
}

69
src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationExtensions.cs

@ -0,0 +1,69 @@
/*
* 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;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Validation;
using OpenIddict.Validation.ServerIntegration;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict validation/server integration services.
/// </summary>
public static class OpenIddictValidationServerIntegrationExtensions
{
/// <summary>
/// Registers the OpenIddict validation/server integration services in the DI container
/// and automatically imports the configuration from the local OpenIddict server.
/// </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="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationServerIntegrationBuilder UseLocalServer([NotNull] this OpenIddictValidationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<OpenIddictValidationOptions>, OpenIddictValidationServerIntegrationConfiguration>());
return new OpenIddictValidationServerIntegrationBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict validation/server integration services in the DI container
/// and automatically imports the configuration from the local OpenIddict server.
/// </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 validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
public static OpenIddictValidationBuilder UseLocalServer(
[NotNull] this OpenIddictValidationBuilder builder,
[NotNull] Action<OpenIddictValidationServerIntegrationBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseLocalServer());
return builder;
}
}
}

15
src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationOptions.cs

@ -0,0 +1,15 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation.ServerIntegration
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict validation/server integration.
/// </summary>
public class OpenIddictValidationServerIntegrationOptions
{
}
}

23
src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>OpenID Connect validation/System.Net.Http integration for OpenIddict.</Description>
<PackageTags>$(PackageTags);http;httpclient;validation</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="$(ExtensionsVersion)" />
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="System.Linq.Async" Version="$(LinqAsyncVersion)" />
</ItemGroup>
</Project>

82
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.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;
using System.ComponentModel;
using System.Net.Http;
using JetBrains.Annotations;
using OpenIddict.Validation.SystemNetHttp;
using Polly;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict validation/System.Net.Http integration.
/// </summary>
public class OpenIddictValidationSystemNetHttpBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationSystemNetHttpBuilder([NotNull] 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 validation/server integration 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="OpenIddictValidationSystemNetHttpBuilder"/>.</returns>
public OpenIddictValidationSystemNetHttpBuilder Configure([NotNull] Action<OpenIddictValidationSystemNetHttpOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Replaces the default HTTP error policy used by the OpenIddict validation services.
/// </summary>
/// <param name="policy">The HTTP Polly error policy.</param>
/// <returns>The <see cref="OpenIddictValidationSystemNetHttpBuilder"/>.</returns>
public OpenIddictValidationSystemNetHttpBuilder SetHttpErrorPolicy([CanBeNull] IAsyncPolicy<HttpResponseMessage> policy)
=> Configure(options => options.HttpErrorPolicy = policy);
/// <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><c>true</c> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals([CanBeNull] 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();
}
}

74
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs

@ -0,0 +1,74 @@
/*
* 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;
using System.Diagnostics;
using System.Net.Http.Headers;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants;
namespace OpenIddict.Validation.SystemNetHttp
{
/// <summary>
/// Contains the methods required to ensure that the OpenIddict validation/System.Net.Http integration configuration is valid.
/// </summary>
public class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions<OpenIddictValidationOptions>,
IConfigureNamedOptions<HttpClientFactoryOptions>
{
public void Configure([NotNull] OpenIddictValidationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict System.Net.Http validation components.
foreach (var handler in OpenIddictValidationSystemNetHttpHandlers.DefaultHandlers)
{
options.DefaultHandlers.Add(handler);
}
}
public void Configure([NotNull] HttpClientFactoryOptions options)
=> Debug.Fail("This infrastructure method shouldn't be called.");
public void Configure([CanBeNull] string name, [NotNull] HttpClientFactoryOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (!string.Equals(name, Clients.Discovery, StringComparison.Ordinal))
{
return;
}
options.HttpClientActions.Add(client =>
{
var name = typeof(OpenIddictValidationSystemNetHttpConfiguration).Assembly.GetName();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(
productName: name.Name,
productVersion: name.Version.ToString()));
});
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
var options = builder.Services.GetRequiredService<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>();
var policy = options.CurrentValue.HttpErrorPolicy;
if (policy != null)
{
builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy));
}
});
}
}
}

16
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs

@ -0,0 +1,16 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation.SystemNetHttp
{
public static class OpenIddictValidationSystemNetHttpConstants
{
public static class Clients
{
public const string Discovery = "OpenIddict.Validation.SystemNetHttp.Discovery";
}
}
}

80
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs

@ -0,0 +1,80 @@
/*
* 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;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using OpenIddict.Validation;
using OpenIddict.Validation.SystemNetHttp;
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpHandlers;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict validation/System.Net.Http integration services.
/// </summary>
public static class OpenIddictValidationSystemNetHttpExtensions
{
/// <summary>
/// Registers the OpenIddict validation/System.Net.Http integration services 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="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationSystemNetHttpBuilder UseSystemNetHttp([NotNull] this OpenIddictValidationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
// Register the built-in validation event handlers used by the OpenIddict System.Net.Http components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictValidationOptions>, OpenIddictValidationSystemNetHttpConfiguration>(),
ServiceDescriptor.Singleton<IConfigureOptions<HttpClientFactoryOptions>, OpenIddictValidationSystemNetHttpConfiguration>()
});
return new OpenIddictValidationSystemNetHttpBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict validation/System.Net.Http integration services 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 validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
public static OpenIddictValidationBuilder UseSystemNetHttp(
[NotNull] this OpenIddictValidationBuilder builder,
[NotNull] Action<OpenIddictValidationSystemNetHttpBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseSystemNetHttp());
return builder;
}
}
}

300
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -0,0 +1,300 @@
/*
* 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.OpenIddictValidationHandlers;
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants;
namespace OpenIddict.Validation.SystemNetHttp
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictValidationSystemNetHttpHandlers
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authentication processing:
*/
PopulateTokenValidationParametersFromMemoryCache.Descriptor,
PopulateTokenValidationParametersFromProviderConfiguration.Descriptor,
CacheTokenValidationParameters.Descriptor);
/// <summary>
/// Contains the logic responsible of populating the token validation parameters from the memory cache.
/// </summary>
public class PopulateTokenValidationParametersFromMemoryCache : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IMemoryCache _cache;
public PopulateTokenValidationParametersFromMemoryCache([NotNull] IMemoryCache cache)
=> _cache = cache;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<PopulateTokenValidationParametersFromMemoryCache>()
.SetOrder(PopulateTokenValidationParametersFromProviderConfiguration.Descriptor.Order - 1_000)
.Build();
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If token validation parameters were already attached, don't overwrite them.
if (context.TokenValidationParameters != null)
{
return default;
}
// If the metadata address is not an HTTP/HTTPS address, let another handler populate the validation parameters.
if (!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
return default;
}
// Resolve the token validation parameters from the memory cache.
if (_cache.TryGetValue(
key: string.Concat("af84c073-c27c-49fd-a54f-584fd60320d3", "\x1e", context.Issuer?.AbsoluteUri),
value: out TokenValidationParameters parameters))
{
context.TokenValidationParameters = parameters;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the token validation
/// parameters using OAuth 2.0/OpenID Connect discovery.
/// </summary>
public class PopulateTokenValidationParametersFromProviderConfiguration : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IHttpClientFactory _factory;
public PopulateTokenValidationParametersFromProviderConfiguration([NotNull] IHttpClientFactory factory)
=> _factory = factory;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<PopulateTokenValidationParametersFromProviderConfiguration>()
.SetOrder(ValidateTokenValidationParameters.Descriptor.Order - 1_000)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If token validation parameters were already attached, don't overwrite them.
if (context.TokenValidationParameters != null)
{
return;
}
// If the metadata address is not an HTTP/HTTPS address, let another handler populate the validation parameters.
if (!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
return;
}
using var client = _factory.CreateClient(Clients.Discovery);
var response = await SendHttpRequestMessageAsync(context.Options.MetadataAddress);
// Ensure the JWKS endpoint URL is present and valid.
if (!response.TryGetParameter(Metadata.JwksUri, out var endpoint) || OpenIddictParameter.IsNullOrEmpty(endpoint))
{
throw new InvalidOperationException("A discovery response containing an empty JWKS endpoint URL was returned.");
}
if (!Uri.TryCreate((string) endpoint, UriKind.Absolute, out Uri uri))
{
throw new InvalidOperationException("A discovery response containing an invalid JWKS endpoint URL was returned.");
}
context.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = (string) response[Metadata.Issuer],
IssuerSigningKeys = await GetSigningKeysAsync(uri).ToListAsync()
};
async IAsyncEnumerable<SecurityKey> GetSigningKeysAsync(Uri address)
{
var response = await SendHttpRequestMessageAsync(address);
var keys = response[JsonWebKeySetParameterNames.Keys];
if (keys == null)
{
throw new InvalidOperationException("The OAuth 2.0/OpenID Connect cryptography didn't contain any JSON web key");
}
foreach (var payload in keys.Value.GetParameters())
{
var type = (string) payload.Value[JsonWebKeyParameterNames.Kty];
if (string.IsNullOrEmpty(type))
{
throw new InvalidOperationException("A JWKS response containing an invalid key was returned.");
}
var key = type switch
{
JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.RSA,
E = (string) payload.Value[JsonWebKeyParameterNames.E],
N = (string) payload.Value[JsonWebKeyParameterNames.N]
},
JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve,
Crv = (string) payload.Value[JsonWebKeyParameterNames.Crv],
X = (string) payload.Value[JsonWebKeyParameterNames.X],
Y = (string) payload.Value[JsonWebKeyParameterNames.Y]
},
_ => throw new InvalidOperationException("A JWKS response containing an unsupported key was returned.")
};
key.KeyId = (string) payload.Value[JsonWebKeyParameterNames.Kid];
key.X5t = (string) payload.Value[JsonWebKeyParameterNames.X5t];
key.X5tS256 = (string) payload.Value[JsonWebKeyParameterNames.X5tS256];
if (payload.Value.TryGetParameter(JsonWebKeyParameterNames.X5c, out var chain))
{
foreach (var certificate in chain.GetParameters())
{
var value = (string) certificate.Value;
if (string.IsNullOrEmpty(value))
{
throw new InvalidOperationException("A JWKS response containing an invalid key was returned.");
}
key.X5c.Add(value);
}
}
yield return key;
}
}
async ValueTask<OpenIddictResponse> SendHttpRequestMessageAsync(Uri address)
{
using var request = new HttpRequestMessage(HttpMethod.Get, address);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"The OAuth 2.0/OpenID Connect discovery failed because an invalid response was received:" +
"the identity provider returned returned a {0} response with the following payload: {1} {2}.",
/* Status: */ response.StatusCode,
/* Headers: */ response.Headers.ToString(),
/* Body: */ await response.Content.ReadAsStringAsync()));
}
var media = response.Content?.Headers.ContentType?.MediaType;
if (!string.Equals(media, "application/json", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"The OAuth 2.0/OpenID Connect discovery failed because an invalid content type was received:" +
"the identity provider returned returned a {0} response with the following payload: {1} {2}.",
/* Status: */ response.StatusCode,
/* Headers: */ response.Headers.ToString(),
/* Body: */ await response.Content.ReadAsStringAsync()));
}
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new JsonTextReader(new StreamReader(stream));
var serializer = JsonSerializer.CreateDefault();
return serializer.Deserialize<OpenIddictResponse>(reader);
}
}
}
/// <summary>
/// Contains the logic responsible of caching the token validation parameters.
/// </summary>
public class CacheTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IMemoryCache _cache;
public CacheTokenValidationParameters([NotNull] IMemoryCache cache)
=> _cache = cache;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<CacheTokenValidationParameters>()
.SetOrder(ValidateTokenValidationParameters.Descriptor.Order + 500)
.Build();
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.TokenValidationParameters == null)
{
return default;
}
// If the metadata address is not an HTTP/HTTPS address, let another handler populate the validation parameters.
if (!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
return default;
}
// Store the token validation parameters in the memory cache.
_ = _cache.GetOrCreate(
key: string.Concat("af84c073-c27c-49fd-a54f-584fd60320d3", "\x1e", context.Issuer?.AbsoluteUri),
factory: entry =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
entry.SetPriority(CacheItemPriority.NeverRemove);
return context.TokenValidationParameters;
});
return default;
}
}
}
}

28
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs

@ -0,0 +1,28 @@
/*
* 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;
using System.Net;
using System.Net.Http;
using Polly;
using Polly.Extensions.Http;
namespace OpenIddict.Validation.SystemNetHttp
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict validation/System.Net.Http integration.
/// </summary>
public class OpenIddictValidationSystemNetHttpOptions
{
/// <summary>
/// Gets or sets the HTTP Polly error policy used by the internal OpenIddict HTTP clients.
/// </summary>
public IAsyncPolicy<HttpResponseMessage> HttpErrorPolicy { get; set; }
= HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(response => response.StatusCode == HttpStatusCode.NotFound)
.WaitAndRetryAsync(4, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
}
}

28
src/OpenIddict.Validation/IOpenIddictValidationHandler.cs

@ -0,0 +1,28 @@
/*
* 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.Threading.Tasks;
using JetBrains.Annotations;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
/// <summary>
/// Represents a handler able to process <typeparamref name="TContext"/> events.
/// </summary>
/// <typeparam name="TContext">The type of the context associated with events handled by this instance.</typeparam>
public interface IOpenIddictValidationHandler<in TContext> where TContext : BaseContext
{
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask HandleAsync([NotNull] TContext context);
}
}

17
src/OpenIddict.Validation/IOpenIddictValidationHandlerFilter.cs

@ -0,0 +1,17 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Threading.Tasks;
using JetBrains.Annotations;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
public interface IOpenIddictValidationHandlerFilter<in TContext> where TContext : BaseContext
{
ValueTask<bool> IsActiveAsync([NotNull] TContext context);
}
}

18
src/OpenIddict.Validation/IOpenIddictValidationProvider.cs

@ -0,0 +1,18 @@
/*
* 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.Threading.Tasks;
using JetBrains.Annotations;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
public interface IOpenIddictValidationProvider
{
ValueTask<OpenIddictValidationTransaction> CreateTransactionAsync();
ValueTask DispatchAsync<TContext>([NotNull] TContext context) where TContext : BaseContext;
}
}

22
src/OpenIddict.Validation/OpenIddict.Validation.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>OpenID Connect validation components for OpenIddict.</Description>
<PackageTags>$(PackageTags);validation</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="$(IdentityModelVersion)" />
</ItemGroup>
</Project>

657
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -0,0 +1,657 @@
/*
* 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;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Validation;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict validation services.
/// </summary>
public class OpenIddictValidationBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationBuilder([NotNull] IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Registers an event handler using the specified configuration delegate.
/// </summary>
/// <typeparam name="TContext">The event context type.</typeparam>
/// <param name="configuration">The configuration delegate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictValidationBuilder AddEventHandler<TContext>(
[NotNull] Action<OpenIddictValidationHandlerDescriptor.Builder<TContext>> configuration)
where TContext : OpenIddictValidationEvents.BaseContext
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
var builder = OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>();
configuration(builder);
return AddEventHandler(builder.Build());
}
/// <summary>
/// Registers an event handler using the specified descriptor.
/// </summary>
/// <param name="descriptor">The handler descriptor.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictValidationBuilder AddEventHandler([NotNull] OpenIddictValidationHandlerDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
// Register the handler in the services collection.
Services.Add(descriptor.ServiceDescriptor);
return Configure(options => options.CustomHandlers.Add(descriptor));
}
/// <summary>
/// Removes the event handler that matches the specified descriptor.
/// </summary>
/// <param name="descriptor">The descriptor corresponding to the handler to remove.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictValidationBuilder RemoveEventHandler([NotNull] OpenIddictValidationHandlerDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
Services.RemoveAll(descriptor.ServiceDescriptor.ServiceType);
Services.PostConfigure<OpenIddictValidationOptions>(options =>
{
for (var index = options.CustomHandlers.Count - 1; index >= 0; index--)
{
if (options.CustomHandlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType)
{
options.CustomHandlers.RemoveAt(index);
}
}
for (var index = options.DefaultHandlers.Count - 1; index >= 0; index--)
{
if (options.DefaultHandlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType)
{
options.DefaultHandlers.RemoveAt(index);
}
}
});
return this;
}
/// <summary>
/// Amends the default OpenIddict validation 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="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder Configure([NotNull] Action<OpenIddictValidationOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Registers the <see cref="EncryptingCredentials"/> used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="credentials">The encrypting credentials.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCredentials([NotNull] EncryptingCredentials credentials)
{
if (credentials == null)
{
throw new ArgumentNullException(nameof(credentials));
}
return Configure(options => options.EncryptionCredentials.Add(credentials));
}
/// <summary>
/// Registers a <see cref="SecurityKey"/> used to decrypt the access tokens issued by OpenIddict.
/// </summary>
/// <param name="key">The security key.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionKey([NotNull] SecurityKey key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
// If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist)
{
throw new InvalidOperationException("The asymmetric encryption key doesn't contain the required private key.");
}
if (IsAlgorithmSupported(key, SecurityAlgorithms.Aes256KW))
{
return AddEncryptionCredentials(new EncryptingCredentials(key,
SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512));
}
if (IsAlgorithmSupported(key, SecurityAlgorithms.RsaOAEP))
{
return AddEncryptionCredentials(new EncryptingCredentials(key,
SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512));
}
throw new InvalidOperationException(new StringBuilder()
.AppendLine("An encryption algorithm cannot be automatically inferred from the encrypting key.")
.Append("Consider using 'options.AddEncryptionCredentials(EncryptingCredentials)' instead.")
.ToString());
static bool IsAlgorithmSupported(SecurityKey key, string algorithm) =>
key.CryptoProviderFactory.IsSupportedAlgorithm(algorithm, key);
}
/// <summary>
/// Registers (and generates if necessary) a user-specific development
/// certificate used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddDevelopmentEncryptionCertificate()
=> AddDevelopmentEncryptionCertificate(new X500DistinguishedName("CN=OpenIddict Validation Encryption Certificate"));
/// <summary>
/// Registers (and generates if necessary) a user-specific development
/// certificate used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="subject">The subject name associated with the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddDevelopmentEncryptionCertificate([NotNull] X500DistinguishedName subject)
{
if (subject == null)
{
throw new ArgumentNullException(nameof(subject));
}
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
// Try to retrieve the development certificate from the specified store.
// If a certificate was found but is not yet or no longer valid, remove it
// from the store before creating and persisting a new encryption certificate.
var certificate = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>()
.SingleOrDefault();
if (certificate != null && (certificate.NotBefore > DateTime.Now || certificate.NotAfter < DateTime.Now))
{
store.Remove(certificate);
certificate = null;
}
#if SUPPORTS_CERTIFICATE_GENERATION
// If no appropriate certificate can be found, generate and persist a new certificate in the specified store.
if (certificate == null)
{
using var algorithm = RSA.Create(keySizeInBits: 2048);
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true));
certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2));
// Note: setting the friendly name is not supported on Unix machines (including Linux and macOS).
// To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
certificate.FriendlyName = "OpenIddict Validation Development Encryption Certificate";
}
// Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate
// as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key.
// To work around this issue, the certificate payload is manually exported and imported back
// into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag.
var data = certificate.Export(X509ContentType.Pfx, string.Empty);
try
{
var flags = X509KeyStorageFlags.PersistKeySet;
// Note: macOS requires marking the certificate private key as exportable.
// If this flag is not set, a CryptographicException is thrown at runtime.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
flags |= X509KeyStorageFlags.Exportable;
}
certificate = new X509Certificate2(data, string.Empty, flags);
}
finally
{
Array.Clear(data, 0, data.Length);
}
store.Add(certificate);
}
return AddEncryptionCertificate(certificate);
#else
throw new PlatformNotSupportedException("X.509 certificate generation is not supported on this platform.");
#endif
}
/// <summary>
/// Registers a new ephemeral key used to decrypt the tokens issued by OpenIddict: the key
/// is discarded when the application shuts down and tokens encrypted using this key are
/// automatically invalidated. This method should only be used during development.
/// On production, using a X.509 certificate stored in the machine store is recommended.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEphemeralEncryptionKey()
=> AddEphemeralEncryptionKey(SecurityAlgorithms.RsaOAEP);
/// <summary>
/// Registers a new ephemeral key used to decrypt the tokens issued by OpenIddict: the key
/// is discarded when the application shuts down and tokens encrypted using this key are
/// automatically invalidated. This method should only be used during development.
/// On production, using a X.509 certificate stored in the machine store is recommended.
/// </summary>
/// <param name="algorithm">The algorithm associated with the encryption key.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEphemeralEncryptionKey([NotNull] string algorithm)
{
if (string.IsNullOrEmpty(algorithm))
{
throw new ArgumentException("The algorithm cannot be null or empty.", nameof(algorithm));
}
switch (algorithm)
{
case SecurityAlgorithms.Aes256KW:
return AddEncryptionCredentials(new EncryptingCredentials(CreateSymmetricSecurityKey(256),
algorithm, SecurityAlgorithms.Aes256CbcHmacSha512));
case SecurityAlgorithms.RsaOAEP:
case SecurityAlgorithms.RsaOaepKeyWrap:
return AddEncryptionCredentials(new EncryptingCredentials(CreateRsaSecurityKey(2048),
algorithm, SecurityAlgorithms.Aes256CbcHmacSha512));
default: throw new InvalidOperationException("The specified algorithm is not supported.");
}
static SymmetricSecurityKey CreateSymmetricSecurityKey(int size)
{
var data = new byte[size / 8];
#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
RandomNumberGenerator.Fill(data);
#else
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(data);
#endif
return new SymmetricSecurityKey(data);
}
static RsaSecurityKey CreateRsaSecurityKey(int size)
{
#if SUPPORTS_DIRECT_KEY_CREATION_WITH_SPECIFIED_SIZE
return new RsaSecurityKey(RSA.Create(size));
#else
// Note: a 1024-bit key might be returned by RSA.Create() on .NET Desktop/Mono,
// where RSACryptoServiceProvider is still the default implementation and
// where custom implementations can be registered via CryptoConfig.
// To ensure the key size is always acceptable, replace it if necessary.
var algorithm = RSA.Create();
if (algorithm.KeySize < size)
{
algorithm.KeySize = size;
}
if (algorithm.KeySize < size && algorithm is RSACryptoServiceProvider)
{
algorithm.Dispose();
algorithm = new RSACryptoServiceProvider(size);
}
if (algorithm.KeySize < size)
{
throw new InvalidOperationException("RSA key generation failed.");
}
return new RsaSecurityKey(algorithm);
#endif
}
}
/// <summary>
/// Registers a <see cref="X509Certificate2"/> that is used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="certificate">The certificate used to decrypt the security tokens issued by the validation.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate([NotNull] X509Certificate2 certificate)
{
if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate));
}
if (certificate.NotBefore > DateTime.Now)
{
throw new InvalidOperationException("The specified certificate is not yet valid.");
}
if (certificate.NotAfter < DateTime.Now)
{
throw new InvalidOperationException("The specified certificate is no longer valid.");
}
if (!certificate.HasPrivateKey)
{
throw new InvalidOperationException("The specified certificate doesn't contain the required private key.");
}
return AddEncryptionKey(new X509SecurityKey(certificate));
}
/// <summary>
/// Registers a <see cref="X509Certificate2"/> retrieved from an
/// embedded resource and used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="assembly">The assembly containing the certificate.</param>
/// <param name="resource">The name of the embedded resource.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate(
[NotNull] Assembly assembly, [NotNull] string resource, [NotNull] string password)
#if SUPPORTS_EPHEMERAL_KEY_SETS
// Note: ephemeral key sets are currently not supported on macOS.
=> AddEncryptionCertificate(assembly, resource, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
X509KeyStorageFlags.MachineKeySet :
X509KeyStorageFlags.EphemeralKeySet);
#else
=> AddEncryptionCertificate(assembly, resource, password, X509KeyStorageFlags.MachineKeySet);
#endif
/// <summary>
/// Registers a <see cref="X509Certificate2"/> retrieved from an
/// embedded resource and used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="assembly">The assembly containing the certificate.</param>
/// <param name="resource">The name of the embedded resource.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <param name="flags">An enumeration of flags indicating how and where to store the private key of the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate(
[NotNull] Assembly assembly, [NotNull] string resource,
[NotNull] string password, X509KeyStorageFlags flags)
{
if (assembly == null)
{
throw new ArgumentNullException(nameof(assembly));
}
if (string.IsNullOrEmpty(resource))
{
throw new ArgumentException("The resource cannot be null or empty.", nameof(resource));
}
if (string.IsNullOrEmpty(password))
{
throw new ArgumentException("The password cannot be null or empty.", nameof(password));
}
using var stream = assembly.GetManifestResourceStream(resource);
if (stream == null)
{
throw new InvalidOperationException("The certificate was not found in the specified assembly.");
}
return AddEncryptionCertificate(stream, password, flags);
}
/// <summary>
/// Registers a <see cref="X509Certificate2"/> extracted from a
/// stream and used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="stream">The stream containing the certificate.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate([NotNull] Stream stream, [NotNull] string password)
#if SUPPORTS_EPHEMERAL_KEY_SETS
// Note: ephemeral key sets are currently not supported on macOS.
=> AddEncryptionCertificate(stream, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
X509KeyStorageFlags.MachineKeySet :
X509KeyStorageFlags.EphemeralKeySet);
#else
=> AddEncryptionCertificate(stream, password, X509KeyStorageFlags.MachineKeySet);
#endif
/// <summary>
/// Registers a <see cref="X509Certificate2"/> extracted from a
/// stream and used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="stream">The stream containing the certificate.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <param name="flags">
/// An enumeration of flags indicating how and where
/// to store the private key of the certificate.
/// </param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate(
[NotNull] Stream stream, [NotNull] string password, X509KeyStorageFlags flags)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
if (string.IsNullOrEmpty(password))
{
throw new ArgumentException("The password cannot be null or empty.", nameof(password));
}
using var buffer = new MemoryStream();
stream.CopyTo(buffer);
return AddEncryptionCertificate(new X509Certificate2(buffer.ToArray(), password, flags));
}
/// <summary>
/// Registers a <see cref="X509Certificate2"/> retrieved from the X.509
/// machine store and used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate([NotNull] string thumbprint)
{
if (string.IsNullOrEmpty(thumbprint))
{
throw new ArgumentException("The thumbprint cannot be null or empty.", nameof(thumbprint));
}
var certificate = GetCertificate(StoreLocation.CurrentUser, thumbprint) ?? GetCertificate(StoreLocation.LocalMachine, thumbprint);
if (certificate == null)
{
throw new InvalidOperationException("The certificate corresponding to the specified thumbprint was not found.");
}
return AddEncryptionCertificate(certificate);
static X509Certificate2 GetCertificate(StoreLocation location, string thumbprint)
{
using var store = new X509Store(StoreName.My, location);
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.SingleOrDefault();
}
}
/// <summary>
/// Registers a <see cref="X509Certificate2"/> retrieved from the given
/// X.509 store and used to decrypt the tokens issued by OpenIddict.
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <param name="name">The name of the X.509 store.</param>
/// <param name="location">The location of the X.509 store.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddEncryptionCertificate(
[NotNull] string thumbprint, StoreName name, StoreLocation location)
{
if (string.IsNullOrEmpty(thumbprint))
{
throw new ArgumentException("The thumbprint cannot be null or empty.", nameof(thumbprint));
}
using var store = new X509Store(name, location);
store.Open(OpenFlags.ReadOnly);
var certificate = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.SingleOrDefault();
if (certificate == null)
{
throw new InvalidOperationException("The certificate corresponding to the specified thumbprint was not found.");
}
return AddEncryptionCertificate(certificate);
}
/// <summary>
/// Registers the specified values as valid audiences. Setting the audiences is recommended
/// when the authorization server issues access tokens for multiple distinct resource servers.
/// </summary>
/// <param name="audiences">The audiences valid for this resource server.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddAudiences([NotNull] params string[] audiences)
{
if (audiences == null)
{
throw new ArgumentNullException(nameof(audiences));
}
if (audiences.Any(audience => string.IsNullOrEmpty(audience)))
{
throw new ArgumentException("Audiences cannot be null or empty.", nameof(audiences));
}
return Configure(options => options.Audiences.UnionWith(audiences));
}
/// <summary>
/// Enables authorization validation so that a database call is made for each API request
/// to ensure the authorization associated with the access token is still valid.
/// Note: enabling this option may have an impact on performance.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder EnableAuthorizationValidation()
=> Configure(options => options.EnableAuthorizationValidation = true);
/// <summary>
/// Sets the issuer address, which is used to determine the actual location of the
/// OAuth 2.0/OpenID Connect configuration document when using provider discovery.
/// </summary>
/// <param name="address">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder SetIssuer([NotNull] Uri address)
{
if (address == null)
{
throw new ArgumentNullException(nameof(address));
}
return Configure(options => options.Issuer = address);
}
/// <summary>
/// Sets the static token validation parameters.
/// </summary>
/// <param name="parameters">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] TokenValidationParameters parameters)
{
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
return Configure(options => options.TokenValidationParameters = parameters);
}
/// <summary>
/// Configures OpenIddict to use reference tokens, so that authorization codes,
/// access tokens and refresh tokens are stored as ciphertext in the database
/// (only an identifier is returned to the client application). Enabling this option
/// is useful to keep track of all the issued tokens, when storing a very large
/// number of claims in the authorization codes, access tokens and refresh tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder UseReferenceTokens()
=> Configure(options => options.UseReferenceTokens = true);
/// <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><c>true</c> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals([CanBeNull] 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();
}
}

138
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -0,0 +1,138 @@
/*
* 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;
using System.Diagnostics;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Validation
{
/// <summary>
/// Contains the methods required to ensure that the OpenIddict validation configuration is valid.
/// </summary>
public class OpenIddictValidationConfiguration : IPostConfigureOptions<OpenIddictValidationOptions>
{
/// <summary>
/// Populates the default OpenIddict validation options and ensures
/// that the configuration is in a consistent and valid state.
/// </summary>
/// <param name="name">The authentication scheme associated with the handler instance.</param>
/// <param name="options">The options instance to initialize.</param>
public void PostConfigure([CanBeNull] string name, [NotNull] OpenIddictValidationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (options.SecurityTokenHandler == null)
{
throw new InvalidOperationException("The security token handler cannot be null.");
}
if (options.TokenValidationParameters == null)
{
if (options.Issuer == null && options.MetadataAddress == null)
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The authority or an absolute metadata endpoint address must be provided.")
.Append("Alternatively, token validation parameters can be manually set by calling ")
.AppendLine("'services.AddOpenIddict().AddValidation().SetTokenValidationParameters()'.")
.Append("To use the server configuration of a local OpenIddict server instance, ")
.Append("reference the 'OpenIddict.Validation.ServerIntegration' package ")
.Append("and call 'services.AddOpenIddict().AddValidation().UseLocalServer()'.")
.ToString());
}
if (options.MetadataAddress == null)
{
options.MetadataAddress = new Uri(".well-known/openid-configuration", UriKind.Relative);
}
if (!options.MetadataAddress.IsAbsoluteUri)
{
if (options.Issuer == null || !options.Issuer.IsAbsoluteUri)
{
throw new InvalidOperationException("The authority must be provided and must be an absolute URL.");
}
if (!string.IsNullOrEmpty(options.Issuer.Fragment) || !string.IsNullOrEmpty(options.Issuer.Query))
{
throw new InvalidOperationException("The authority cannot contain a fragment or a query string.");
}
if (!options.Issuer.OriginalString.EndsWith("/"))
{
options.Issuer = new Uri(options.Issuer.OriginalString + "/", UriKind.Absolute);
}
options.MetadataAddress = new Uri(options.Issuer, options.MetadataAddress);
}
}
foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key))
{
if (!string.IsNullOrEmpty(key.KeyId))
{
continue;
}
key.KeyId = GetKeyIdentifier(key);
}
static string GetKeyIdentifier(SecurityKey key)
{
// When no key identifier can be retrieved from the security keys, a value is automatically
// inferred from the hexadecimal representation of the certificate thumbprint (SHA-1)
// when the key is bound to a X.509 certificate or from the public part of the signing key.
if (key is X509SecurityKey x509SecurityKey)
{
return x509SecurityKey.Certificate.Thumbprint;
}
if (key is RsaSecurityKey rsaSecurityKey)
{
// Note: if the RSA parameters are not attached to the signing key,
// extract them by calling ExportParameters on the RSA instance.
var parameters = rsaSecurityKey.Parameters;
if (parameters.Modulus == null)
{
parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Modulus != null,
"A null modulus shouldn't be returned by RSA.ExportParameters().");
}
// Only use the 40 first chars of the base64url-encoded modulus.
var identifier = Base64UrlEncoder.Encode(parameters.Modulus);
return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant();
}
#if SUPPORTS_ECDSA
if (key is ECDsaSecurityKey ecsdaSecurityKey)
{
// Extract the ECDSA parameters from the signing credentials.
var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Q.X != null,
"Invalid coordinates shouldn't be returned by ECDsa.ExportParameters().");
// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant();
}
#endif
return null;
}
}
}
}

19
src/OpenIddict.Validation/OpenIddictValidationEndpointType.cs

@ -0,0 +1,19 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation
{
/// <summary>
/// Represents the type of an OpenIddict validation endpoint.
/// </summary>
public enum OpenIddictValidationEndpointType
{
/// <summary>
/// Unknown endpoint.
/// </summary>
Unknown = 0
}
}

271
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -0,0 +1,271 @@
/*
* 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Claims;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
namespace OpenIddict.Validation
{
public static partial class OpenIddictValidationEvents
{
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseContext
{
/// <summary>
/// Creates a new instance of the <see cref="BaseContext"/> class.
/// </summary>
protected BaseContext([NotNull] OpenIddictValidationTransaction transaction)
=> Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
/// <summary>
/// Gets the environment associated with the current request being processed.
/// </summary>
public OpenIddictValidationTransaction Transaction { get; }
/// <summary>
/// Gets or sets the endpoint type that handled the request, if applicable.
/// </summary>
public OpenIddictValidationEndpointType EndpointType
{
get => Transaction.EndpointType;
set => Transaction.EndpointType = value;
}
/// <summary>
/// Gets or sets the issuer address associated with the current transaction, if available.
/// </summary>
public Uri Issuer
{
get => Transaction.Issuer;
set => Transaction.Issuer = value;
}
/// <summary>
/// Gets the logger responsible of logging processed operations.
/// </summary>
public ILogger Logger => Transaction.Logger;
/// <summary>
/// Gets the OpenIddict validation options.
/// </summary>
public OpenIddictValidationOptions Options => Transaction.Options;
/// <summary>
/// Gets the dictionary containing the properties associated with this event.
/// </summary>
public IDictionary<string, object> Properties { get; }
= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the OpenIddict request or <c>null</c> if it couldn't be extracted.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the OpenIddict response, if applicable.
/// </summary>
public OpenIddictResponse Response
{
get => Transaction.Response;
set => Transaction.Response = value;
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseRequestContext : BaseContext
{
/// <summary>
/// Creates a new instance of the <see cref="BaseRequestContext"/> class.
/// </summary>
protected BaseRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets a boolean indicating whether the request was fully handled.
/// </summary>
public bool IsRequestHandled { get; private set; }
/// <summary>
/// Gets a boolean indicating whether the request processing was skipped.
/// </summary>
public bool IsRequestSkipped { get; private set; }
/// <summary>
/// Marks the request as fully handled. Once declared handled,
/// a request shouldn't be processed further by the underlying host.
/// </summary>
public void HandleRequest() => IsRequestHandled = true;
/// <summary>
/// Marks the request as skipped. Once declared skipped, a request
/// shouldn't be processed further by OpenIddict but should be allowed
/// to go through the next components in the processing pipeline
/// (if this pattern is supported by the underlying host).
/// </summary>
public void SkipRequest() => IsRequestSkipped = true;
}
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseValidatingContext : BaseRequestContext
{
/// <summary>
/// Creates a new instance of the <see cref="BaseValidatingContext"/> class.
/// </summary>
protected BaseValidatingContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets a boolean indicating whether the request will be rejected.
/// </summary>
public bool IsRejected { get; protected set; }
/// <summary>
/// Gets or sets the "error" parameter returned to the client application.
/// </summary>
public string Error { get; private set; }
/// <summary>
/// Gets or sets the "error_description" parameter returned to the client application.
/// </summary>
public string ErrorDescription { get; private set; }
/// <summary>
/// Gets or sets the "error_uri" parameter returned to the client application.
/// </summary>
public string ErrorUri { get; private set; }
/// <summary>
/// Rejects the request.
/// </summary>
public virtual void Reject() => IsRejected = true;
/// <summary>
/// Rejects the request.
/// </summary>
/// <param name="error">The "error" parameter returned to the client application.</param>
public virtual void Reject(string error)
{
Error = error;
Reject();
}
/// <summary>
/// Rejects the request.
/// </summary>
/// <param name="error">The "error" parameter returned to the client application.</param>
/// <param name="description">The "error_description" parameter returned to the client application.</param>
public virtual void Reject(string error, string description)
{
Error = error;
ErrorDescription = description;
Reject();
}
/// <summary>
/// Rejects the request.
/// </summary>
/// <param name="error">The "error" parameter returned to the client application.</param>
/// <param name="description">The "error_description" parameter returned to the client application.</param>
/// <param name="uri">The "error_uri" parameter returned to the client application.</param>
public virtual void Reject(string error, string description, string uri)
{
Error = error;
ErrorDescription = description;
ErrorUri = uri;
Reject();
}
}
/// <summary>
/// Represents an event called when processing an incoming request.
/// </summary>
public class ProcessRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ProcessRequestContext"/> class.
/// </summary>
public ProcessRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called when processing an errored response.
/// </summary>
public class ProcessErrorContext : BaseRequestContext
{
/// <summary>
/// Creates a new instance of the <see cref="ProcessErrorContext"/> class.
/// </summary>
public ProcessErrorContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called when processing an authentication operation.
/// </summary>
public class ProcessAuthenticationContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ProcessAuthenticationContext"/> class.
/// </summary>
public ProcessAuthenticationContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the token validation parameters used for the current request.
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; set; }
/// <summary>
/// Gets or sets the security principal.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
}
/// <summary>
/// Represents an event called when processing a challenge response.
/// </summary>
public class ProcessChallengeContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ProcessChallengeContext"/> class.
/// </summary>
public ProcessChallengeContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
}
}

83
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -0,0 +1,83 @@
/*
* 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;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Validation;
using static OpenIddict.Validation.OpenIddictValidationHandlerFilters;
using static OpenIddict.Validation.OpenIddictValidationHandlers;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict validation services.
/// </summary>
public static class OpenIddictValidationExtensions
{
/// <summary>
/// Registers the OpenIddict token validation services 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="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationBuilder AddValidation([NotNull] this OpenIddictBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddLogging();
builder.Services.AddOptions();
builder.Services.TryAddScoped<IOpenIddictValidationProvider, OpenIddictValidationProvider>();
// Register the built-in validation event handlers used by the OpenIddict validation components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Register the built-in filters used by the default OpenIddict validation event handlers.
builder.Services.TryAddSingleton<RequireAuthorizationValidationEnabled>();
builder.Services.TryAddSingleton<RequireReferenceTokensDisabled>();
builder.Services.TryAddSingleton<RequireReferenceTokensEnabled>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictValidationOptions>, OpenIddictValidationConfiguration>());
return new OpenIddictValidationBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict token validation services 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 validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
public static OpenIddictBuilder AddValidation(
[NotNull] this OpenIddictBuilder builder,
[NotNull] Action<OpenIddictValidationBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.AddValidation());
return builder;
}
}
}

39
src/OpenIddict.Validation/OpenIddictValidationHandler.cs

@ -0,0 +1,39 @@
/*
* 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;
using System.Threading.Tasks;
using JetBrains.Annotations;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
/// <summary>
/// Represents a handler able to process <typeparamref name="TContext"/> events.
/// </summary>
/// <typeparam name="TContext">The type of the events handled by this instance.</typeparam>
public class OpenIddictValidationHandler<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseContext
{
private readonly Func<TContext, ValueTask> _handler;
/// <summary>
/// Creates a new event using the specified handler delegate.
/// </summary>
/// <param name="handler">The event handler delegate.</param>
public OpenIddictValidationHandler([NotNull] Func<TContext, ValueTask> handler)
=> _handler = handler ?? throw new ArgumentNullException(nameof(handler));
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
=> _handler(context ?? throw new ArgumentNullException(nameof(context)));
}
}

199
src/OpenIddict.Validation/OpenIddictValidationHandlerDescriptor.cs

@ -0,0 +1,199 @@
/*
* 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
/// <summary>
/// Represents an immutable descriptor of an OpenIddict validation event handler.
/// </summary>
[DebuggerDisplay("{ServiceDescriptor?.ServiceType}")]
public class OpenIddictValidationHandlerDescriptor
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationHandlerDescriptor"/> class.
/// </summary>
private OpenIddictValidationHandlerDescriptor() { }
/// <summary>
/// Gets the context type associated with the event.
/// </summary>
public Type ContextType { get; private set; }
/// <summary>
/// Gets the list of filters responsible of excluding the handler
/// from the activated handlers if it doesn't meet the criteria.
/// </summary>
public ImmutableArray<Type> FilterTypes { get; private set; } = ImmutableArray.Create<Type>();
/// <summary>
/// Gets the order assigned to the handler.
/// </summary>
public int Order { get; private set; }
/// <summary>
/// Gets the service descriptor associated with the handler.
/// </summary>
public ServiceDescriptor ServiceDescriptor { get; private set; }
/// <summary>
/// Creates a builder allowing to initialize an immutable descriptor.
/// </summary>
/// <typeparam name="TContext">The event context type.</typeparam>
/// <returns>A new descriptor builder.</returns>
public static Builder<TContext> CreateBuilder<TContext>() where TContext : BaseContext
=> new Builder<TContext>();
/// <summary>
/// Contains methods allowing to build a descriptor instance.
/// </summary>
/// <typeparam name="TContext">The event context type.</typeparam>
public class Builder<TContext> where TContext : BaseContext
{
private ServiceDescriptor _descriptor;
private readonly List<Type> _filterTypes = new List<Type>();
private int _order;
/// <summary>
/// Adds the type of a handler filter to the filters list.
/// </summary>
/// <param name="type">The event handler filter type.</param>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> AddFilter([NotNull] Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (!typeof(IOpenIddictValidationHandlerFilter<>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type))
{
throw new InvalidOperationException("The specified service type is not valid.");
}
_filterTypes.Add(type);
return this;
}
/// <summary>
/// Adds the type of a handler filter to the filters list.
/// </summary>
/// <typeparam name="TFilter">The event handler filter type.</typeparam>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> AddFilter<TFilter>()
where TFilter : IOpenIddictValidationHandlerFilter<TContext>
=> AddFilter(typeof(TFilter));
/// <summary>
/// Sets the service descriptor.
/// </summary>
/// <param name="descriptor">The service descriptor.</param>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> SetServiceDescriptor([NotNull] ServiceDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var type = descriptor.ServiceType;
if (!typeof(IOpenIddictValidationHandler<>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type))
{
throw new InvalidOperationException("The specified service type is not valid.");
}
_descriptor = descriptor;
return this;
}
/// <summary>
/// Sets the order in which the event handler will be invoked.
/// </summary>
/// <param name="order">The handler order.</param>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> SetOrder(int order)
{
_order = order;
return this;
}
/// <summary>
/// Configures the descriptor to use the specified inline handler.
/// </summary>
/// <param name="handler">The handler instance.</param>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> UseInlineHandler([NotNull] Func<TContext, ValueTask> handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
return UseSingletonHandler(new OpenIddictValidationHandler<TContext>(handler));
}
/// <summary>
/// Configures the descriptor to use the specified scoped handler.
/// </summary>
/// <typeparam name="THandler">The handler type.</typeparam>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> UseScopedHandler<THandler>()
where THandler : IOpenIddictValidationHandler<TContext>
=> SetServiceDescriptor(new ServiceDescriptor(
typeof(THandler), typeof(THandler), ServiceLifetime.Scoped));
/// <summary>
/// Configures the descriptor to use the specified singleton handler.
/// </summary>
/// <typeparam name="THandler">The handler type.</typeparam>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> UseSingletonHandler<THandler>()
where THandler : IOpenIddictValidationHandler<TContext>
=> SetServiceDescriptor(new ServiceDescriptor(
typeof(THandler), typeof(THandler), ServiceLifetime.Singleton));
/// <summary>
/// Configures the descriptor to use the specified singleton handler.
/// </summary>
/// <typeparam name="THandler">The handler type.</typeparam>
/// <param name="handler">The handler instance.</param>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public Builder<TContext> UseSingletonHandler<THandler>([NotNull] THandler handler)
where THandler : IOpenIddictValidationHandler<TContext>
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
return SetServiceDescriptor(new ServiceDescriptor(typeof(THandler), handler));
}
/// <summary>
/// Build a new descriptor instance, based on the parameters that were previously set.
/// </summary>
/// <returns>The builder instance, so that calls can be easily chained.</returns>
public OpenIddictValidationHandlerDescriptor Build() => new OpenIddictValidationHandlerDescriptor
{
ContextType = typeof(TContext),
FilterTypes = _filterTypes.ToImmutableArray(),
Order = _order,
ServiceDescriptor = _descriptor ?? throw new InvalidOperationException("No service descriptor was set.")
};
}
}
}

66
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -0,0 +1,66 @@
/*
* 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;
using System.ComponentModel;
using System.Threading.Tasks;
using JetBrains.Annotations;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictValidationHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if authorization validation was not enabled.
/// </summary>
public class RequireAuthorizationValidationEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.EnableAuthorizationValidation);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are enabled.
/// </summary>
public class RequireReferenceTokensDisabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.UseReferenceTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are disabled.
/// </summary>
public class RequireReferenceTokensEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseReferenceTokens);
}
}
}
}

533
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -0,0 +1,533 @@
/*
* 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.OpenIddictValidationHandlerFilters;
namespace OpenIddict.Validation
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictValidationHandlers
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authentication processing:
*/
ValidateTokenValidationParameters.Descriptor,
ValidateAccessTokenParameter.Descriptor,
ValidateReferenceToken.Descriptor,
ValidateSelfContainedToken.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateAudience.Descriptor,
ValidateAuthorizationEntry.Descriptor,
/*
* Challenge processing:
*/
AttachDefaultChallengeError.Descriptor);
/// <summary>
/// Contains the logic responsible of ensuring the token validation parameters are populated.
/// </summary>
public class ValidateTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateTokenValidationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: at this stage, throw an exception if the token validation parameters cannot be found.
var parameters = context.TokenValidationParameters ?? context.Options.TokenValidationParameters;
if (parameters == null)
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The token validation parameters cannot be retrieved.")
.Append("To register the default client, reference the 'OpenIddict.Validation.SystemNetHttp' package ")
.AppendLine("and call 'services.AddOpenIddict().AddValidation().UseSystemNetHttp()'.")
.Append("Alternatively, you can manually provide the token validation parameters ")
.Append("by calling 'services.AddOpenIddict().AddValidation().SetTokenValidationParameters()'.")
.ToString());
}
// Clone the token validation parameters before mutating them to ensure the
// shared token validation parameters registered as options are not modified.
parameters = parameters.Clone();
parameters.NameClaimType = Claims.Name;
parameters.PropertyBag = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.AccessToken };
parameters.RoleClaimType = Claims.Role;
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.ValidateAudience = false;
parameters.ValidateLifetime = false;
context.TokenValidationParameters = parameters;
return default;
}
}
/// <summary>
/// Contains the logic responsible of validating the access token resolved from the current request.
/// </summary>
public class ValidateAccessTokenParameter : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateAccessTokenParameter>()
.SetOrder(ValidateTokenValidationParameters.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request.AccessToken))
{
context.Logger.LogError("The request was rejected because the access token was missing.");
context.Reject(
error: Errors.InvalidToken,
description: "The access token is missing.");
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands that use an invalid reference token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class ValidateReferenceToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public ValidateReferenceToken() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling reference tokens support.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.ToString());
public ValidateReferenceToken([NotNull] IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceToken>()
.SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
{
return;
}
// If the reference token cannot be found, return a generic error.
var token = await _tokenManager.FindByReferenceIdAsync(context.Request.AccessToken);
if (token == null || !string.Equals(await _tokenManager.GetTypeAsync(token),
TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase))
{
context.Reject(
error: Errors.InvalidToken,
description: "The specified token is not valid.");
return;
}
var payload = await _tokenManager.GetPayloadAsync(token);
if (string.IsNullOrEmpty(payload))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The payload associated with a reference token cannot be retrieved.")
.Append("This may indicate that the token entry was corrupted.")
.ToString());
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (!context.Options.SecurityTokenHandler.CanReadToken(payload))
{
return;
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var result = await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(
payload, context.TokenValidationParameters);
if (result.ClaimsIdentity == null)
{
return;
}
// Attach the principal extracted from the authorization code to the parent event context
// and restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity)
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetInternalTokenId(await _tokenManager.GetIdAsync(token))
.SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token));
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands that specify an invalid self-contained token.
/// </summary>
public class ValidateSelfContainedToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateSelfContainedToken>()
.SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
{
return;
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (!context.Options.SecurityTokenHandler.CanReadToken(context.Request.AccessToken))
{
return;
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var result = await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(
context.Request.AccessToken, context.TokenValidationParameters);
if (result.ClaimsIdentity == null)
{
return;
}
// Attach the principal extracted from the token to the parent event context.
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity);
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved.
/// </summary>
public class ValidatePrincipal : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Principal == null)
{
context.Reject(
error: Errors.InvalidToken,
description: "The specified token is not valid.");
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands containing expired access tokens.
/// </summary>
public class ValidateExpirationDate : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var date = context.Principal.GetExpirationDate();
if (date.HasValue && date.Value < DateTimeOffset.UtcNow)
{
context.Logger.LogError("The request was rejected because the access token was expired.");
context.Reject(
error: Errors.InvalidToken,
description: "The specified access token is no longer valid.");
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands containing
/// access tokens that were issued to be used by another audience/resource server.
/// </summary>
public class ValidateAudience : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateAudience>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If no explicit audience has been configured,
// skip the default audience validation.
if (context.Options.Audiences.Count == 0)
{
return default;
}
// If the access token doesn't have any audience attached, return an error.
if (!context.Principal.HasAudience())
{
context.Logger.LogError("The request was rejected because the access token had no audience attached.");
context.Reject(
error: Errors.InvalidToken,
description: "The specified access token doesn't contain any audience.");
return default;
}
// If the access token doesn't include any registered audience, return an error.
if (context.Principal.GetAudiences().Intersect(context.Options.Audiences).IsEmpty)
{
context.Logger.LogError("The request was rejected because the access token had no valid audience.");
context.Reject(
error: Errors.InvalidToken,
description: "The specified access token cannot be used with this resource server.");
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of authentication demands a token whose
/// associated authorization entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class ValidateAuthorizationEntry : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictAuthorizationManager _authorizationManager;
public ValidateAuthorizationEntry() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling reference tokens support.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.ToString());
public ValidateAuthorizationEntry([NotNull] IOpenIddictAuthorizationManager authorizationManager)
=> _authorizationManager = authorizationManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAuthorizationValidationEnabled>()
.UseScopedHandler<ValidateAuthorizationEntry>()
.SetOrder(ValidateAudience.Descriptor.Order + 1_000)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var identifier = context.Principal.GetInternalAuthorizationId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var authorization = await _authorizationManager.FindByIdAsync(identifier);
if (authorization == null || !await _authorizationManager.IsValidAsync(authorization))
{
context.Logger.LogError("The authorization '{Identifier}' was no longer valid.", identifier);
context.Reject(
error: Errors.InvalidToken,
description: "The authorization associated with the token is no longer valid.");
return;
}
}
}
/// <summary>
/// Contains the logic responsible of ensuring that the challenge response contains an appropriate error.
/// </summary>
public class AttachDefaultChallengeError : IOpenIddictValidationHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachDefaultChallengeError>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessChallengeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Response.Error))
{
context.Response.Error = Errors.InvalidToken;
}
if (string.IsNullOrEmpty(context.Response.ErrorDescription))
{
context.Response.ErrorDescription = "The access token is not valid.";
}
return default;
}
}
}
}

86
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -0,0 +1,86 @@
/*
* 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;
using System.Collections.Generic;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Validation
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict validation handler.
/// </summary>
public class OpenIddictValidationOptions
{
/// <summary>
/// Gets the list of credentials used to encrypt the tokens issued by the
/// OpenIddict validation services. Note: only symmetric credentials are supported.
/// </summary>
public IList<EncryptingCredentials> EncryptionCredentials { get; } = new List<EncryptingCredentials>();
/// <summary>
/// Gets or sets the security token handler used to protect and unprotect tokens.
/// </summary>
public OpenIddictValidationTokenHandler SecurityTokenHandler { get; set; } = new OpenIddictValidationTokenHandler
{
SetDefaultTimesOnTokenCreation = false
};
/// <summary>
/// Gets the list of the user-defined/custom handlers responsible of processing the OpenIddict validation requests.
/// Note: the handlers added to this list must be also registered in the DI container using an appropriate lifetime.
/// </summary>
public IList<OpenIddictValidationHandlerDescriptor> CustomHandlers { get; } =
new List<OpenIddictValidationHandlerDescriptor>();
/// <summary>
/// Gets the list of the built-in handlers responsible of processing the OpenIddict validation requests
/// </summary>
public IList<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } =
new List<OpenIddictValidationHandlerDescriptor>(OpenIddictValidationHandlers.DefaultHandlers);
/// <summary>
/// Gets or sets a boolean indicating whether a database call is made
/// to validate the authorization associated with the received tokens.
/// </summary>
public bool EnableAuthorizationValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens should be used.
/// When set to <c>true</c>, authorization codes, access tokens and refresh tokens
/// are stored as ciphertext in the database and a crypto-secure random identifier
/// is returned to the client application. Enabling this option is useful
/// to keep track of all the issued tokens, when storing a very large number
/// of claims in the authorization codes, access tokens and refresh tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// </summary>
public bool UseReferenceTokens { get; set; }
/// <summary>
/// Gets or sets the absolute URL of the OAuth 2.0/OpenID Connect server.
/// </summary>
public Uri Issuer { get; set; }
/// <summary>
/// Gets or sets the URL of the OAuth 2.0/OpenID Connect server discovery endpoint.
/// When the URL is relative, <see cref="Issuer"/> must be set and absolute.
/// </summary>
public Uri MetadataAddress { get; set; }
/// <summary>
/// Gets the intended audiences of this resource server.
/// Setting this property is recommended when the authorization
/// server issues access tokens for multiple distinct resource servers.
/// </summary>
public ISet<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the token validation parameters used by the OpenIddict validation services.
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; set; }
}
}

132
src/OpenIddict.Validation/OpenIddictValidationProvider.cs

@ -0,0 +1,132 @@
/*
* 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;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation
{
public class OpenIddictValidationProvider : IOpenIddictValidationProvider
{
private readonly ILogger<OpenIddictValidationProvider> _logger;
private readonly IOptionsMonitor<OpenIddictValidationOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationProvider"/> class.
/// </summary>
public OpenIddictValidationProvider(
[NotNull] ILogger<OpenIddictValidationProvider> logger,
[NotNull] IOptionsMonitor<OpenIddictValidationOptions> options,
[NotNull] IServiceProvider provider)
{
_logger = logger;
_options = options;
_provider = provider;
}
public ValueTask<OpenIddictValidationTransaction> CreateTransactionAsync()
=> new ValueTask<OpenIddictValidationTransaction>(new OpenIddictValidationTransaction
{
Issuer = _options.CurrentValue.Issuer,
Logger = _logger,
Options = _options.CurrentValue
});
public async ValueTask DispatchAsync<TContext>([NotNull] TContext context) where TContext : BaseContext
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
await foreach (var handler in GetHandlersAsync())
{
await handler.HandleAsync(context);
switch (context)
{
case BaseRequestContext notification when notification.IsRequestHandled:
_logger.LogDebug("The request was handled in user code.");
return;
case BaseRequestContext notification when notification.IsRequestSkipped:
_logger.LogDebug("The default request handling was skipped from user code.");
return;
case BaseValidatingContext notification when notification.IsRejected:
_logger.LogDebug("The request was rejected in user code.");
return;
default: continue;
}
}
async IAsyncEnumerable<IOpenIddictValidationHandler<TContext>> GetHandlersAsync()
{
var descriptors = new List<OpenIddictValidationHandlerDescriptor>(
capacity: _options.CurrentValue.CustomHandlers.Count +
_options.CurrentValue.DefaultHandlers.Count);
descriptors.AddRange(_options.CurrentValue.CustomHandlers);
descriptors.AddRange(_options.CurrentValue.DefaultHandlers);
descriptors.Sort((left, right) => left.Order.CompareTo(right.Order));
for (var index = 0; index < descriptors.Count; index++)
{
var descriptor = descriptors[index];
if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor))
{
continue;
}
var handler = descriptor.ServiceDescriptor.ImplementationInstance != null ?
descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictValidationHandler<TContext> :
_provider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictValidationHandler<TContext>;
if (handler == null)
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine($"The event handler of type '{descriptor.ServiceDescriptor.ServiceType}' couldn't be resolved.")
.AppendLine("This may indicate that it was not properly registered in the dependency injection container.")
.Append("To register an event handler, use 'services.AddOpenIddict().AddValidation().AddEventHandler()'.")
.ToString());
}
yield return handler;
}
}
async ValueTask<bool> IsActiveAsync(OpenIddictValidationHandlerDescriptor descriptor)
{
for (var index = 0; index < descriptor.FilterTypes.Length; index++)
{
if (!(_provider.GetService(descriptor.FilterTypes[index]) is IOpenIddictValidationHandlerFilter<TContext> filter))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine($"The event handler filter of type '{descriptor.FilterTypes[index]}' couldn't be resolved.")
.AppendLine("This may indicate that it was not properly registered in the dependency injection container.")
.ToString());
}
if (!await filter.IsActiveAsync(context))
{
return false;
}
}
return true;
}
}
}
}

95
src/OpenIddict.Validation/OpenIddictValidationTokenHandler.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 System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenIddict.Validation
{
public class OpenIddictValidationTokenHandler : JsonWebTokenHandler
{
public ValueTask<TokenValidationResult> ValidateTokenStringAsync(string token, TokenValidationParameters parameters)
{
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
if (parameters.PropertyBag == null)
{
throw new InvalidOperationException("The property bag cannot be null.");
}
if (!parameters.PropertyBag.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type))
{
throw new InvalidOperationException("The token usage cannot be null or empty.");
}
if (!CanReadToken(token))
{
return new ValueTask<TokenValidationResult>(new TokenValidationResult
{
Exception = new SecurityTokenException("The token was not compatible with the JWT format."),
IsValid = false
});
}
try
{
var result = base.ValidateToken(token, parameters);
if (result == null || !result.IsValid)
{
return new ValueTask<TokenValidationResult>(new TokenValidationResult
{
Exception = result?.Exception,
IsValid = false
});
}
var assertion = ((JsonWebToken) result.SecurityToken)?.InnerToken ?? (JsonWebToken) result.SecurityToken;
if (!assertion.TryGetPayloadValue(Claims.Private.TokenUsage, out string usage) ||
!string.Equals(usage, (string) type, StringComparison.OrdinalIgnoreCase))
{
return new ValueTask<TokenValidationResult>(new TokenValidationResult
{
Exception = new SecurityTokenException("The token usage associated to the token does not match the expected type."),
IsValid = false
});
}
// Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object).
if (assertion.TryGetPayloadValue(Claims.Private.ClaimDestinations, out IDictionary<string, string[]> definitions))
{
foreach (var definition in definitions)
{
foreach (var claim in result.ClaimsIdentity.Claims.Where(claim => claim.Type == definition.Key))
{
claim.SetDestinations(definition.Value);
}
}
}
return new ValueTask<TokenValidationResult>(result);
}
catch (Exception exception)
{
return new ValueTask<TokenValidationResult>(new TokenValidationResult
{
Exception = exception,
IsValid = false
});
}
}
}
}

55
src/OpenIddict.Validation/OpenIddictValidationTransaction.cs

@ -0,0 +1,55 @@
/*
* 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;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using OpenIddict.Abstractions;
namespace OpenIddict.Validation
{
/// <summary>
/// Represents the context associated with an OpenID Connect validation request.
/// </summary>
public class OpenIddictValidationTransaction
{
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// </summary>
public OpenIddictValidationEndpointType EndpointType { get; set; }
/// <summary>
/// Gets or sets the issuer address associated with the current transaction, if available.
/// </summary>
public Uri Issuer { get; set; }
/// <summary>
/// Gets or sets the logger associated with the current request.
/// </summary>
public ILogger Logger { get; set; }
/// <summary>
/// Gets or sets the options associated with the current request.
/// </summary>
public OpenIddictValidationOptions Options { get; set; }
/// <summary>
/// Gets the additional properties associated with the current request.
/// </summary>
public IDictionary<string, object> Properties { get; }
= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the current OpenID Connect request.
/// </summary>
public OpenIddictRequest Request { get; set; }
/// <summary>
/// Gets or sets the current OpenID Connect response being returned.
/// </summary>
public OpenIddictResponse Response { get; set; }
}
}

3
src/OpenIddict/OpenIddict.csproj

@ -14,6 +14,9 @@
<ProjectReference Include="..\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" /> <ProjectReference Include="..\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" />
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" /> <ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" /> <ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" />
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.ServerIntegration\OpenIddict.Validation.ServerIntegration.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.SystemNetHttp\OpenIddict.Validation.SystemNetHttp.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

Loading…
Cancel
Save