Browse Source

Implement Pushed Authorization Requests (PAR) support and revamp the request caching feature

pull/2251/head
Kévin Chalet 2 years ago
parent
commit
42d6369910
  1. 10
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  2. 10
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  3. 8
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  4. 10
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
  5. 4
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  6. 20
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  7. 34
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  8. 181
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  9. 20
      src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
  10. 16
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  11. 1
      src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs
  12. 9
      src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
  13. 1
      src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj
  14. 1
      src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj
  15. 40
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Authorization.cs
  16. 4
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
  17. 1
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  18. 113
      src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs
  19. 67
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  20. 1
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  21. 17
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  22. 213
      src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
  23. 3
      src/OpenIddict.Client/OpenIddictClientHandlers.Device.cs
  24. 203
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  25. 549
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  26. 9
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  27. 179
      src/OpenIddict.Client/OpenIddictClientService.cs
  28. 17
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
  29. 4
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  30. 4
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
  31. 22
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  32. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs
  33. 268
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  34. 253
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  35. 25
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  36. 4
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs
  37. 7
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs
  38. 2
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
  39. 3
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
  40. 42
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
  41. 6
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs
  42. 17
      src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
  43. 3
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  44. 4
      src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
  45. 13
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  46. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs
  47. 259
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  48. 242
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  49. 25
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  50. 4
      src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs
  51. 79
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  52. 27
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  53. 7
      src/OpenIddict.Server/OpenIddictServerEndpointType.cs
  54. 204
      src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs
  55. 16
      src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
  56. 8
      src/OpenIddict.Server/OpenIddictServerEvents.Session.cs
  57. 77
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  58. 4
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  59. 68
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  60. 2158
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  61. 57
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  62. 13
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  63. 160
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  64. 663
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  65. 34
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  66. 1
      src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj
  67. 7
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictRequestTests.cs
  68. 7
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs
  69. 57
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Authentication.cs
  70. 57
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Session.cs
  71. 2582
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
  72. 66
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
  73. 77
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs
  74. 245
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  75. 57
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Authentication.cs
  76. 57
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Session.cs

10
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs

@ -225,6 +225,16 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
return Set(registration => registration.Scopes.UnionWith(scopes));
}
/// <summary>
/// Disables pushed authorization requests for this client registration. When pushed authorization
/// requests are disabled, PAR is not used by the OpenIddict client, even if the remote authorization
/// server exposes a pushed authorization endpoint. If the authorization server requires using PAR,
/// an exception is automatically thrown when starting an interactive authentication challenge.
/// </summary>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public {{ provider.name }} DisablePushedAuthorizationRequests()
=> Set(registration => registration.DisablePushedAuthorizationRequests = true);
/// <summary>
/// Sets the issuer that will be attached to the <see cref=""Claim""/>
/// instances created by the OpenIddict client stack for this provider.

10
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs

@ -48,6 +48,11 @@ public class AuthorizationController : Controller
[HttpGet, Route("~/connect/authorize")]
public async Task<ActionResult> Authorize()
{
// Note: the request object contains all the parameters specified in the query string or request form
// or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow.
// As such, the data contained in this object MUST NOT be serialized or returned unprotected to the
// user agent (e.g as HTML hidden input fields). If only the query string or request form parameters
// need to be resolved, the Request.QueryString and Request.Form collections must be used instead.
var context = HttpContext.GetOwinContext();
var request = context.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
@ -213,6 +218,11 @@ public class AuthorizationController : Controller
[HttpPost, Route("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<ActionResult> Accept()
{
// Note: the request object contains all the parameters specified in the query string or request form
// (or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow).
// As such, the data contained in this object MUST NOT be serialized or returned unprotected to the
// user agent (e.g as HTML hidden input fields). If only the query string or request form parameters
// need to be resolved, the Request.QueryString and Request.Form collections must be used instead.
var context = HttpContext.GetOwinContext();
var request = context.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

8
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -87,6 +87,7 @@ public class Startup
.SetEndSessionEndpointUris("connect/endsession")
.SetEndUserVerificationEndpointUris("connect/verify")
.SetIntrospectionEndpointUris("connect/introspect")
.SetPushedAuthorizationEndpointUris("connect/par")
.SetTokenEndpointUris("connect/token")
.SetUserInfoEndpointUris("connect/userinfo");
@ -103,9 +104,6 @@ public class Startup
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Force client applications to use Proof Key for Code Exchange (PKCE).
options.RequireProofKeyForCodeExchange();
// Register the OWIN host and configure the OWIN-specific options.
options.UseOwin()
.EnableAuthorizationEndpointPassthrough()
@ -215,6 +213,7 @@ public class Startup
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.PushedAuthorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
@ -226,7 +225,8 @@ public class Startup
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
Requirements.Features.ProofKeyForCodeExchange,
Requirements.Features.PushedAuthorizationRequests
}
});
}

10
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs

@ -58,6 +58,11 @@ public class AuthorizationController : Controller
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
// Note: the request object contains all the parameters specified in the query string or request form
// or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow.
// As such, the data contained in this object MUST NOT be serialized or returned unprotected to the
// user agent (e.g as HTML hidden input fields). If only the query string or request form parameters
// need to be resolved, the Request.Query and Request.Form collections must be used instead.
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
@ -234,6 +239,11 @@ public class AuthorizationController : Controller
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
// Note: the request object contains all the parameters specified in the query string or request form
// (or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow).
// As such, the data contained in this object MUST NOT be serialized or returned unprotected to the
// user agent (e.g as HTML hidden input fields). If only the query string or request form parameters
// need to be resolved, the Request.Query and Request.Form collections must be used instead.
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

4
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -105,6 +105,7 @@ public class Startup
.SetEndSessionEndpointUris("connect/endsession")
.SetEndUserVerificationEndpointUris("connect/verify")
.SetIntrospectionEndpointUris("connect/introspect")
.SetPushedAuthorizationEndpointUris("connect/par")
.SetRevocationEndpointUris("connect/revoke")
.SetTokenEndpointUris("connect/token")
.SetUserInfoEndpointUris("connect/userinfo");
@ -125,9 +126,6 @@ public class Startup
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Force client applications to use Proof Key for Code Exchange (PKCE).
options.RequireProofKeyForCodeExchange();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()

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

@ -77,6 +77,7 @@ public class Worker : IHostedService
Permissions.Endpoints.DeviceAuthorization,
Permissions.Endpoints.Introspection,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.PushedAuthorization,
Permissions.Endpoints.Revocation,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
@ -98,7 +99,8 @@ public class Worker : IHostedService
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
Requirements.Features.ProofKeyForCodeExchange,
Requirements.Features.PushedAuthorizationRequests
}
});
}
@ -128,6 +130,7 @@ public class Worker : IHostedService
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.PushedAuthorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
@ -139,7 +142,8 @@ public class Worker : IHostedService
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
Requirements.Features.ProofKeyForCodeExchange,
Requirements.Features.PushedAuthorizationRequests
}
});
}
@ -189,6 +193,7 @@ public class Worker : IHostedService
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.PushedAuthorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
@ -200,7 +205,8 @@ public class Worker : IHostedService
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
Requirements.Features.ProofKeyForCodeExchange,
Requirements.Features.PushedAuthorizationRequests
}
});
}
@ -230,6 +236,7 @@ public class Worker : IHostedService
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.PushedAuthorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
@ -241,7 +248,8 @@ public class Worker : IHostedService
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
Requirements.Features.ProofKeyForCodeExchange,
Requirements.Features.PushedAuthorizationRequests
}
});
}
@ -271,6 +279,7 @@ public class Worker : IHostedService
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.PushedAuthorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
@ -282,7 +291,8 @@ public class Worker : IHostedService
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
Requirements.Features.ProofKeyForCodeExchange,
Requirements.Features.PushedAuthorizationRequests
}
});
}

34
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -145,6 +145,9 @@ public static class OpenIddictConstants
public const string RedirectUri = "oi_reduri";
public const string RefreshTokenLifetime = "oi_reft_lft";
public const string RegistrationId = "oi_reg_id";
public const string RequestParameters = "oi_req_prms";
public const string RequestTokenLifetime = "oi_reqt_lft";
public const string RequestTokenType = "oi_reqt_typ";
public const string Resource = "oi_rsrc";
public const string ResponseType = "oi_rsp_typ";
public const string SigningAlgorithm = "oi_sign_alg";
@ -264,6 +267,7 @@ public static class OpenIddictConstants
public const string AuthorizationCode = "oi_auc+jwt";
public const string DeviceCode = "oi_dvc+jwt";
public const string RefreshToken = "oi_reft+jwt";
public const string RequestToken = "oi_reqt+jwt";
public const string StateToken = "oi_stet+jwt";
public const string UserCode = "oi_usrc+jwt";
}
@ -296,11 +300,14 @@ public static class OpenIddictConstants
public const string OpPolicyUri = "op_policy_uri";
public const string OpTosUri = "op_tos_uri";
public const string PromptValuesSupported = "prompt_values_supported";
public const string PushedAuthorizationRequestEndpoint = "pushed_authorization_request_endpoint";
public const string PushedAuthorizationRequestEndpointAuthMethodsSupported = "pushed_authorization_request_endpoint_auth_methods_supported";
public const string RequestObjectEncryptionAlgValuesSupported = "request_object_encryption_alg_values_supported";
public const string RequestObjectEncryptionEncValuesSupported = "request_object_encryption_enc_values_supported";
public const string RequestObjectSigningAlgValuesSupported = "request_object_signing_alg_values_supported";
public const string RequestParameterSupported = "request_parameter_supported";
public const string RequestUriParameterSupported = "request_uri_parameter_supported";
public const string RequirePushedAuthorizationRequests = "require_pushed_authorization_requests";
public const string RequireRequestUriRegistration = "require_request_uri_registration";
public const string ResponseModesSupported = "response_modes_supported";
public const string ResponseTypesSupported = "response_types_supported";
@ -362,6 +369,7 @@ public static class OpenIddictConstants
public const string RefreshToken = "refresh_token";
public const string Registration = "registration";
public const string Request = "request";
[Obsolete("This property is obsolete and will be removed in a future version.")]
public const string RequestId = "request_id";
public const string RequestUri = "request_uri";
public const string Resource = "resource";
@ -387,6 +395,7 @@ public static class OpenIddictConstants
public const string DeviceAuthorization = "ept:device_authorization";
public const string EndSession = "ept:end_session";
public const string Introspection = "ept:introspection";
public const string PushedAuthorization = "ept:pushed_authorization";
public const string Revocation = "ept:revocation";
public const string Token = "ept:token";
}
@ -445,11 +454,30 @@ public static class OpenIddictConstants
public const string Destinations = ".destinations";
}
public static class RequestTokenTypes
{
public static class Private
{
public const string CachedAuthorizationRequest = "cached_authorization_request";
public const string CachedEndSessionRequest = "cached_end_session_request";
public const string PushedAuthorizationRequest = "pushed_authorization_request";
}
}
public static class RequestUris
{
public static class Prefixes
{
public const string Generic = "urn:ietf:params:oauth:request_uri:";
}
}
public static class Requirements
{
public static class Features
{
public const string ProofKeyForCodeExchange = "ft:pkce";
public const string PushedAuthorizationRequests = "ft:par";
}
public static class Prefixes
@ -518,6 +546,7 @@ public static class OpenIddictConstants
public const string DeviceCode = "tkn_lft:dvc";
public const string IdentityToken = "tkn_lft:idt";
public const string RefreshToken = "tkn_lft:reft";
public const string RequestToken = "tkn_lft:reqt";
public const string UserCode = "tkn_lft:usrc";
}
}
@ -566,6 +595,11 @@ public static class OpenIddictConstants
public const string StateToken = "state_token";
public const string UserInfoToken = "userinfo_token";
public const string UserCode = "user_code";
public static class Private
{
public const string RequestToken = "request_token";
}
}
public static class TokenTypes

181
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -152,11 +152,11 @@ When implementing custom token deserialization, a 'oi_tkn_typ' claim containing
Make sure that 'ClaimsPrincipal.Identity' is not null.</value>
</data>
<data name="ID0012" xml:space="preserve">
<value>The specified principal contains an authenticated identity, which is not valid when the sign-in operation is triggered from the device authorization endpoint.
<value>The specified principal contains an authenticated identity, which is not valid when the sign-in operation is triggered from the device authorization or pushed authorization endpoints.
Make sure that 'ClaimsPrincipal.Identity.AuthenticationType' is null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'false'.</value>
</data>
<data name="ID0013" xml:space="preserve">
<value>The specified principal contains a subject claim, which is not valid when the sign-in operation is triggered from the device authorization endpoint.</value>
<value>The specified principal contains a subject claim, which is not valid when the sign-in operation is triggered from the device authorization or pushed authorization endpoints.</value>
</data>
<data name="ID0014" xml:space="preserve">
<value>The specified principal doesn't contain a valid/authenticated identity.
@ -218,8 +218,8 @@ Alternatively, create a class implementing 'IOpenIddictServerHandler&lt;HandleAu
To apply authorization responses, create a class implementing 'IOpenIddictServerHandler&lt;ApplyAuthorizationResponseContext&gt;' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.</value>
</data>
<data name="ID0031" xml:space="preserve">
<value>The device request was not correctly extracted.
To extract device requests, create a class implementing 'IOpenIddictServerHandler&lt;ExtractDeviceAuthorizationRequestContext&gt;' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.</value>
<value>The device authorization request was not correctly extracted.
To extract device authorization requests, create a class implementing 'IOpenIddictServerHandler&lt;ExtractDeviceAuthorizationRequestContext&gt;' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.</value>
</data>
<data name="ID0032" xml:space="preserve">
<value>The client application details cannot be found in the database.</value>
@ -416,7 +416,7 @@ To use key rollover, register both the new certificate and the old one in the cr
<value>No custom authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler&lt;ValidateAuthorizationRequestContext&gt;' must be implemented to validate authorization requests (e.g to ensure the client_id and redirect_uri are valid).</value>
</data>
<data name="ID0090" xml:space="preserve">
<value>No custom device request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler&lt;ValidateDeviceAuthorizationRequestContext&gt;' (or 'IOpenIddictServerHandler&lt;ProcessAuthenticationContext&gt;') must be implemented to validate device requests (e.g to ensure the client_id and client_secret are valid).</value>
<value>No custom device authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler&lt;ValidateDeviceAuthorizationRequestContext&gt;' (or 'IOpenIddictServerHandler&lt;ProcessAuthenticationContext&gt;') must be implemented to validate device authorization requests (e.g to ensure the client_id and client_secret are valid).</value>
</data>
<data name="ID0091" xml:space="preserve">
<value>No custom introspection request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler&lt;ValidateIntrospectionRequestContext&gt;' (or 'IOpenIddictServerHandler&lt;ProcessAuthenticationContext&gt;') must be implemented to validate introspection requests (e.g to ensure the client_id and client_secret are valid).</value>
@ -501,12 +501,15 @@ This may indicate that the event handler responsible for processing OpenID Conne
<data name="ID0116" xml:space="preserve">
<value>A distributed cache instance must be registered when enabling request caching.
To register the default in-memory distributed cache implementation, reference the 'Microsoft.Extensions.Caching.Memory' package and call 'services.AddDistributedMemoryCache()' from 'ConfigureServices'.</value>
<comment>This resource is no longer used and will be removed in a future version.</comment>
</data>
<data name="ID0117" xml:space="preserve">
<value>The authorization request payload is malformed.</value>
<comment>This resource is no longer used and will be removed in a future version.</comment>
</data>
<data name="ID0118" xml:space="preserve">
<value>The end session request payload is malformed.</value>
<comment>This resource is no longer used and will be removed in a future version.</comment>
</data>
<data name="ID0119" xml:space="preserve">
<value>The OpenIddict OWIN server handler cannot be used as an active authentication handler.
@ -1707,9 +1710,50 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0459" xml:space="preserve">
<value>A token must be specified when using revocation.</value>
</data>
<data name="ID0460" xml:space="preserve">
<value>The authorization server requires using pushed authorization requests. Consider setting 'OpenIddictClientRegistration.DisablePushedAuthorizationRequests' to false to allow the OpenIddict client to use pushed authorization requests.</value>
</data>
<data name="ID0461" xml:space="preserve">
<value>An error occurred while preparing the device authorization request.
Error: {0}
Error description: {1}
Error URI: {2}</value>
</data>
<data name="ID0462" xml:space="preserve">
<value>An error occurred while sending the device authorization request.
Error: {0}
Error description: {1}
Error URI: {2}</value>
</data>
<data name="ID0463" xml:space="preserve">
<value>An error occurred while extracting the device authorization response.
Error: {0}
Error description: {1}
Error URI: {2}</value>
</data>
<data name="ID0464" xml:space="preserve">
<value>An error occurred while handling the device authorization response.
Error: {0}
Error description: {1}
Error URI: {2}</value>
</data>
<data name="ID0465" xml:space="preserve">
<value>Authorization request caching and end session request caching cannot be used when disabling token storage.</value>
</data>
<data name="ID0466" xml:space="preserve">
<value>No custom pushed authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler&lt;ValidatePushedAuthorizationRequestContext&gt;' (or 'IOpenIddictServerHandler&lt;ProcessAuthenticationContext&gt;') must be implemented to validate pushed authorization requests (e.g to ensure the client_id and client_secret are valid).</value>
</data>
<data name="ID0467" xml:space="preserve">
<value>The VK ID integration requires sending the device identifier to the token and revocation endpoints. For that, attach a ".device_id" authentication property containing the device identifier returned by the authorization endpoint.</value>
</data>
<data name="ID0468" xml:space="preserve">
<value>The pushed authorization request was not correctly extracted.
To extract pushed authorization requests, create a class implementing 'IOpenIddictServerHandler&lt;ExtractPushedAuthorizationRequestContext&gt;' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.</value>
</data>
<data name="ID0469" xml:space="preserve">
<value>The pushed authorization response was not correctly applied.
To apply pushed authorization responses, create a class implementing 'IOpenIddictServerHandler&lt;ApplyPushedAuthorizationResponseContext&gt;' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -2134,7 +2178,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>The client application is not allowed to use the specified identity token hint.</value>
</data>
<data name="ID2142" xml:space="preserve">
<value>The specified state token is not suitable for the requested operation.</value>
<value>The specified '{0}' parameter is not suitable for the requested operation.</value>
</data>
<data name="ID2143" xml:space="preserve">
<value>An unsupported content encoding was returned by the remote server.</value>
@ -2238,6 +2282,27 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID2176" xml:space="preserve">
<value>The introspection response indicates the token is no longer valid.</value>
</data>
<data name="ID2177" xml:space="preserve">
<value>The '{0}' parameter must be attached as a regular OAuth 2.0 parameter when using a request object or pushed authorization requests.</value>
</data>
<data name="ID2178" xml:space="preserve">
<value>The '{0}' parameter doesn't match the value specified in the request object or pushed authorization request.</value>
</data>
<data name="ID2179" xml:space="preserve">
<value>The pushed authorization request was rejected by the remote server.</value>
</data>
<data name="ID2180" xml:space="preserve">
<value>The mandatory '{0}' parameter couldn't be found in the pushed authorization response.</value>
</data>
<data name="ID2181" xml:space="preserve">
<value>The '{0}' parameter returned in the pushed authorization response is not valid absolute URI.</value>
</data>
<data name="ID2182" xml:space="preserve">
<value>A '{0}' obtained from the pushed authorization request endpoint is required for this client application.</value>
</data>
<data name="ID2183" xml:space="preserve">
<value>This client application is not allowed to use the authorization endpoint.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -2424,19 +2489,19 @@ The principal used to create the token contained the following claims: {Claims}.
<value>The request URI matched a server endpoint: {Endpoint}.</value>
</data>
<data name="ID6054" xml:space="preserve">
<value>The device request was successfully extracted: {Request}.</value>
<value>The device authorization request was successfully extracted: {Request}.</value>
</data>
<data name="ID6055" xml:space="preserve">
<value>The device request was successfully validated.</value>
<value>The device authorization request was successfully validated.</value>
</data>
<data name="ID6057" xml:space="preserve">
<value>The device request was rejected because invalid scopes were specified: {Scopes}.</value>
<value>The device authorization request was rejected because invalid scopes were specified: {Scopes}.</value>
</data>
<data name="ID6062" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to use the device authorization endpoint.</value>
<value>The device authorization request was rejected because the application '{ClientId}' was not allowed to use the device authorization endpoint.</value>
</data>
<data name="ID6063" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to use the scope {Scope}.</value>
<value>The device authorization request was rejected because the application '{ClientId}' was not allowed to use the scope {Scope}.</value>
</data>
<data name="ID6064" xml:space="preserve">
<value>The verification request was successfully extracted: {Request}.</value>
@ -2562,13 +2627,13 @@ The principal used to create the token contained the following claims: {Claims}.
<value>The revocation request was rejected because the received token was of an unsupported type.</value>
</data>
<data name="ID6118" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to use the device authorization flow.</value>
<value>The device authorization request was rejected because the application '{ClientId}' was not allowed to use the device authorization flow.</value>
</data>
<data name="ID6119" xml:space="preserve">
<value>The revocation request was rejected because the access token was issued to a different client or for another resource server.</value>
</data>
<data name="ID6120" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.</value>
<value>The device authorization request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.</value>
</data>
<data name="ID6121" xml:space="preserve">
<value>The revocation request was rejected because the refresh token was issued to a different client.</value>
@ -2901,6 +2966,96 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6233" xml:space="preserve">
<value>The authorization request was rejected because an unsupported prompt parameter was specified.</value>
</data>
<data name="ID6234" xml:space="preserve">
<value>The pushed authorization request was rejected by the remote authorization server: {Response}.</value>
</data>
<data name="ID6235" xml:space="preserve">
<value>The pushed authorization request was successfully sent to {Uri}: {Request}.</value>
</data>
<data name="ID6236" xml:space="preserve">
<value>The pushed authorization response returned by {Uri} was successfully extracted: {Response}.</value>
</data>
<data name="ID6237" xml:space="preserve">
<value>The pushed authorization request was successfully extracted: {Request}.</value>
</data>
<data name="ID6238" xml:space="preserve">
<value>The pushed authorization request was successfully validated.</value>
</data>
<data name="ID6239" xml:space="preserve">
<value>The pushed authorization request was rejected because it contained an unsupported parameter: {Parameter}.</value>
</data>
<data name="ID6240" xml:space="preserve">
<value>The pushed authorization request was rejected because the mandatory '{Parameter}' parameter was missing.</value>
</data>
<data name="ID6241" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{Parameter}' parameter wasn't a valid absolute URI: {RedirectUri}.</value>
</data>
<data name="ID6242" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.</value>
</data>
<data name="ID6243" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{ResponseType}' response type is not supported.</value>
</data>
<data name="ID6244" xml:space="preserve">
<value>The pushed authorization request was rejected because the 'response_type'/'response_mode' combination was invalid: {ResponseType} ; {ResponseMode}.</value>
</data>
<data name="ID6245" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{ResponseMode}' response mode is not supported.</value>
</data>
<data name="ID6246" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{Scope}' scope was missing.</value>
</data>
<data name="ID6247" xml:space="preserve">
<value>The pushed authorization request was rejected because an invalid prompt combination was specified.</value>
</data>
<data name="ID6248" xml:space="preserve">
<value>The pushed authorization request was rejected because the specified code challenge method was not supported.</value>
</data>
<data name="ID6249" xml:space="preserve">
<value>The pushed authorization request was rejected because the response type was not compatible with 'code_challenge'/'code_challenge_method'.</value>
</data>
<data name="ID6250" xml:space="preserve">
<value>The pushed authorization request was rejected because the specified response type was not compatible with PKCE.</value>
</data>
<data name="ID6251" xml:space="preserve">
<value>The pushed authorization request was rejected because the confidential application '{ClientId}' was not allowed to retrieve an access token from the authorization endpoint.</value>
</data>
<data name="ID6252" xml:space="preserve">
<value>The pushed authorization request was rejected because the redirect_uri was invalid: '{RedirectUri}'.</value>
</data>
<data name="ID6253" xml:space="preserve">
<value>The authentication request was rejected because invalid scopes were specified: {Scopes}.</value>
</data>
<data name="ID6254" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the pushed authorization endpoint.</value>
</data>
<data name="ID6255" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the authorization code flow.</value>
</data>
<data name="ID6256" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the implicit flow.</value>
</data>
<data name="ID6257" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the hybrid flow.</value>
</data>
<data name="ID6258" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the '{Scope}' scope.</value>
</data>
<data name="ID6259" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{Parameter}' contained a forbidden parameter: {Name}.</value>
</data>
<data name="ID6260" xml:space="preserve">
<value>The pushed authorization request was rejected because the '{ResponseType}' response type is not a valid combination.</value>
</data>
<data name="ID6261" xml:space="preserve">
<value>The pushed authorization request was rejected because an unsupported prompt parameter was specified.</value>
</data>
<data name="ID6262" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the '{ResponseType}' response type.</value>
</data>
<data name="ID6263" xml:space="preserve">
<value>The pushed authorization request was rejected because the identity token used as a hint was issued to a different client.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

20
src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs

@ -86,6 +86,11 @@ public sealed class OpenIddictConfiguration
/// </summary>
public Uri? MtlsIntrospectionEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled pushed authorization endpoint.
/// </summary>
public Uri? MtlsPushedAuthorizationEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled revocation endpoint.
/// </summary>
@ -106,6 +111,21 @@ public sealed class OpenIddictConfiguration
/// </summary>
public Dictionary<string, object?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the URI of the pushed authorization endpoint.
/// </summary>
public Uri? PushedAuthorizationEndpoint { get; set; }
/// <summary>
/// Gets the client authentication methods supported by the pushed authorization endpoint.
/// </summary>
public HashSet<string> PushedAuthorizationEndpointAuthMethodsSupported { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets a boolean indicating whether pushed authorization requests are required.
/// </summary>
public bool? RequirePushedAuthorizationRequests { get; set; }
/// <summary>
/// Gets the response mode supported by the server.
/// </summary>

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

@ -2558,6 +2558,22 @@ public static class OpenIddictExtensions
public static TimeSpan? GetIdentityTokenLifetime(this ClaimsPrincipal principal)
=> GetLifetime(principal, Claims.Private.IdentityTokenLifetime);
/// <summary>
/// Gets the request token lifetime associated with the claims identity.
/// </summary>
/// <param name="identity">The claims identity.</param>
/// <returns>The request token lifetime or <see langword="null"/> if the claim cannot be found.</returns>
public static TimeSpan? GetRequestTokenLifetime(this ClaimsIdentity identity)
=> GetLifetime(identity, Claims.Private.RequestTokenLifetime);
/// <summary>
/// Gets the request token lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns>The request token lifetime or <see langword="null"/> if the claim cannot be found.</returns>
public static TimeSpan? GetRequestTokenLifetime(this ClaimsPrincipal principal)
=> GetLifetime(principal, Claims.Private.RequestTokenLifetime);
/// <summary>
/// Gets the refresh token lifetime associated with the claims identity.
/// </summary>

1
src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs

@ -375,6 +375,7 @@ public class OpenIddictRequest : OpenIddictMessage
/// <summary>
/// Gets or sets the "request_id" parameter.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public string? RequestId
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.RequestId);

9
src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs

@ -198,6 +198,15 @@ public class OpenIddictResponse : OpenIddictMessage
set => SetParameter(OpenIddictConstants.Parameters.RefreshToken, value);
}
/// <summary>
/// Gets or sets the "request_uri" parameter.
/// </summary>
public string? RequestUri
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.RequestUri);
set => SetParameter(OpenIddictConstants.Parameters.RequestUri, value);
}
/// <summary>
/// Gets or sets the "scope" parameter.
/// </summary>

1
src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj

@ -24,7 +24,6 @@
('$(TargetFrameworkIdentifier)' == '.NETStandard') ">
<PackageReference Include="Microsoft.AspNetCore.Authentication" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
</ItemGroup>
<ItemGroup>

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

@ -14,7 +14,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
<PackageReference Include="Microsoft.Extensions.WebEncoders" />
<PackageReference Include="Microsoft.Owin.Security" />
</ItemGroup>

40
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Authorization.cs

@ -0,0 +1,40 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
namespace OpenIddict.Client.SystemNetHttp;
public static partial class OpenIddictClientSystemNetHttpHandlers
{
public static class Authorization
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
/*
* Pushed authorization request processing:
*/
CreateHttpClient<PreparePushedAuthorizationRequestContext>.Descriptor,
PreparePostHttpRequest<PreparePushedAuthorizationRequestContext>.Descriptor,
AttachHttpVersion<PreparePushedAuthorizationRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PreparePushedAuthorizationRequestContext>.Descriptor,
AttachUserAgentHeader<PreparePushedAuthorizationRequestContext>.Descriptor,
AttachFromHeader<PreparePushedAuthorizationRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials<PreparePushedAuthorizationRequestContext>.Descriptor,
AttachHttpParameters<PreparePushedAuthorizationRequestContext>.Descriptor,
SendHttpRequest<ApplyPushedAuthorizationRequestContext>.Descriptor,
DisposeHttpRequest<ApplyPushedAuthorizationRequestContext>.Descriptor,
/*
* Pushed authorization response processing:
*/
DecompressResponseContent<ExtractPushedAuthorizationResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractPushedAuthorizationResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractPushedAuthorizationResponseContext>.Descriptor,
ValidateHttpResponse<ExtractPushedAuthorizationResponseContext>.Descriptor,
DisposeHttpResponse<ExtractPushedAuthorizationResponseContext>.Descriptor
]);
}
}

4
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs

@ -14,7 +14,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
/*
* DeviceAuthorization request processing:
* Device authorization request processing:
*/
CreateHttpClient<PrepareDeviceAuthorizationRequestContext>.Descriptor,
PreparePostHttpRequest<PrepareDeviceAuthorizationRequestContext>.Descriptor,
@ -28,7 +28,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
DisposeHttpRequest<ApplyDeviceAuthorizationRequestContext>.Descriptor,
/*
* DeviceAuthorization response processing:
* Device authorization response processing:
*/
DecompressResponseContent<ExtractDeviceAuthorizationResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractDeviceAuthorizationResponseContext>.Descriptor,

1
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -47,6 +47,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
*/
AttachNonDefaultRevocationEndpointClientAuthenticationMethod.Descriptor,
.. Authorization.DefaultHandlers,
.. Device.DefaultHandlers,
.. Discovery.DefaultHandlers,
.. Exchange.DefaultHandlers,

113
src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs

@ -78,6 +78,119 @@ public static partial class OpenIddictClientEvents
public string AuthorizationEndpoint { get; set; } = null!;
}
/// <summary>
/// Represents an event called for each request to the pushed authorization endpoint request
/// to give the user code a chance to add parameters to the pushed authorization request.
/// </summary>
public sealed class PreparePushedAuthorizationRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="PreparePushedAuthorizationRequestContext"/> class.
/// </summary>
public PreparePushedAuthorizationRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
}
/// <summary>
/// Represents an event called for each request to the pushed authorization endpoint request
/// to send the pushed authorization request to the remote authorization server.
/// </summary>
public sealed class ApplyPushedAuthorizationRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyPushedAuthorizationRequestContext"/> class.
/// </summary>
public ApplyPushedAuthorizationRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
}
/// <summary>
/// Represents an event called for each pushed authorization response
/// to extract the response parameters from the server response.
/// </summary>
public sealed class ExtractPushedAuthorizationResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractPushedAuthorizationResponseContext"/> class.
/// </summary>
public ExtractPushedAuthorizationResponseContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response, or <see langword="null"/> if it wasn't extracted yet.
/// </summary>
public OpenIddictResponse? Response
{
get => Transaction.Response;
set => Transaction.Response = value;
}
}
/// <summary>
/// Represents an event called for each pushed authorization response.
/// </summary>
public sealed class HandlePushedAuthorizationResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandlePushedAuthorizationResponseContext"/> class.
/// </summary>
public HandlePushedAuthorizationResponseContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response.
/// </summary>
public OpenIddictResponse Response
{
get => Transaction.Response!;
set => Transaction.Response = value;
}
}
/// <summary>
/// Represents an event called for each request to the redirection endpoint to give the user code
/// a chance to manually extract the redirection request from the ambient HTTP context.

67
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -1052,6 +1052,17 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? DeviceAuthorizationEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the URI of the pushed authorization endpoint, if applicable.
/// </summary>
public Uri? PushedAuthorizationEndpoint { get; set; }
/// <summary>
/// Gets or sets the client authentication method used when communicating
/// with the pushed authorization endpoint, if applicable.
/// </summary>
public string? PushedAuthorizationEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a state token
/// should be generated (and optionally included in the request).
@ -1082,6 +1093,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public bool SendDeviceAuthorizationRequest { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a pushed authorization request should be sent.
/// </summary>
public bool SendPushedAuthorizationRequest { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a client assertion
/// token should be generated (and optionally included in the request).
@ -1136,6 +1152,16 @@ public static partial class OpenIddictClientEvents
/// </summary>
public OpenIddictResponse? DeviceAuthorizationResponse { get; set; }
/// <summary>
/// Gets or sets the request sent to the pushed authorization endpoint, if applicable.
/// </summary>
public OpenIddictRequest? PushedAuthorizationRequest { get; set; }
/// <summary>
/// Gets or sets the response returned by the pushed authorization endpoint, if applicable.
/// </summary>
public OpenIddictResponse? PushedAuthorizationResponse { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a device
/// code should be extracted from the current context.
@ -1145,6 +1171,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool ExtractDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a request token
/// should be extracted from the current context.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ExtractRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user
/// code should be extracted from the current context.
@ -1163,6 +1198,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool RequireDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a request token
/// must be resolved for the authentication to be considered valid.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RequireRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user code must
/// be resolved for the authentication to be considered valid.
@ -1181,6 +1225,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool ValidateDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the request token
/// extracted from the current context should be validated.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ValidateRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the user code
/// extracted from the current context should be validated.
@ -1199,6 +1252,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool RejectDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid request token
/// will cause the authentication demand to be rejected or will be ignored.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid user code will
/// cause the authentication demand to be rejected or will be ignored.
@ -1213,6 +1275,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? DeviceCode { get; set; }
/// <summary>
/// Gets or sets the request token to validate, if applicable.
/// </summary>
public string? RequestToken { get; set; }
/// <summary>
/// Gets or sets the user code to validate, if applicable.
/// </summary>

1
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -56,6 +56,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireLogoutStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectionRequest>();
builder.Services.TryAddSingleton<RequirePushedAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireRedirectionRequest>();
builder.Services.TryAddSingleton<RequireRefreshTokenValidated>();
builder.Services.TryAddSingleton<RequireRevocationClientAssertionGenerated>();

17
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -371,6 +371,23 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no pushed authorization request is expected to be sent.
/// </summary>
public sealed class RequirePushedAuthorizationRequest : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.SendPushedAuthorizationRequest);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a redirection request.
/// </summary>

213
src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs

@ -5,6 +5,7 @@
*/
using System.Collections.Immutable;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using OpenIddict.Extensions;
@ -31,6 +32,14 @@ public static partial class OpenIddictClientHandlers
*/
AttachAuthorizationEndpoint.Descriptor,
/*
* Pushed authorization response handling:
*/
ValidateWellKnownPushedAuthorizationResponseParameters.Descriptor,
HandlePushedAuthorizationErrorResponse.Descriptor,
ValidatePushedAuthorizationRequestUri.Descriptor,
ValidatePushedAuthorizationExpiration.Descriptor,
/*
* Redirection request top-level processing:
*/
@ -193,6 +202,210 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the well-known parameters contained in the pushed authorization response.
/// </summary>
public sealed class ValidateWellKnownPushedAuthorizationResponseParameters : IOpenIddictClientHandler<HandlePushedAuthorizationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandlePushedAuthorizationResponseContext>()
.UseSingletonHandler<ValidateWellKnownPushedAuthorizationResponseParameters>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandlePushedAuthorizationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
foreach (var parameter in context.Response.GetParameters())
{
if (!ValidateParameterType(parameter.Key, parameter.Value))
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2107(parameter.Key),
uri: SR.FormatID8000(SR.ID2107));
return default;
}
}
return default;
// Note: in the typical case, the response parameters should be deserialized from a
// JSON response and thus natively stored as System.Text.Json.JsonElement instances.
//
// In the rare cases where the underlying value wouldn't be a JsonElement instance
// (e.g when custom parameters are manually added to the response), the static
// conversion operator would take care of converting the underlying value to a
// JsonElement instance using the same value type as the original parameter value.
static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch
{
// Error parameters MUST be formatted as unique strings:
Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as unique strings:
Parameters.RequestUri => ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as numeric dates:
Parameters.ExpiresIn => (JsonElement) value is { ValueKind: JsonValueKind.Number } element &&
element.TryGetDecimal(out decimal result) && result is >= 0,
// Parameters that are not in the well-known list can be of any type.
_ => true
};
}
}
/// <summary>
/// Contains the logic responsible for surfacing potential errors from the pushed authorization response.
/// </summary>
public sealed class HandlePushedAuthorizationErrorResponse : IOpenIddictClientHandler<HandlePushedAuthorizationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandlePushedAuthorizationResponseContext>()
.UseSingletonHandler<HandlePushedAuthorizationErrorResponse>()
.SetOrder(ValidateWellKnownPushedAuthorizationResponseParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandlePushedAuthorizationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// For more information, see https://www.rfc-editor.org/rfc/rfc8628#section-3.2.
if (!string.IsNullOrEmpty(context.Response.Error))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6234), context.Response);
context.Reject(
error: context.Response.Error switch
{
Errors.InvalidClient => Errors.InvalidRequest,
Errors.InvalidScope => Errors.InvalidScope,
Errors.InvalidRequest => Errors.InvalidRequest,
Errors.UnauthorizedClient => Errors.UnauthorizedClient,
_ => Errors.ServerError
},
description: SR.GetResourceString(SR.ID2179),
uri: SR.FormatID8000(SR.ID2179));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the request URI contained in the pushed authorization response.
/// </summary>
public sealed class ValidatePushedAuthorizationRequestUri : IOpenIddictClientHandler<HandlePushedAuthorizationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandlePushedAuthorizationResponseContext>()
.UseSingletonHandler<ValidatePushedAuthorizationRequestUri>()
.SetOrder(HandlePushedAuthorizationErrorResponse.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandlePushedAuthorizationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Return an error if the mandatory "request_uri" parameter is missing.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc9126#section-2.2.
if (string.IsNullOrEmpty(context.Response.RequestUri))
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2180(Parameters.RequestUri),
uri: SR.FormatID8000(SR.ID2180));
return default;
}
// Return an error if the "request_uri" parameter is malformed.
if (!Uri.TryCreate(context.Response.RequestUri, UriKind.Absolute, out Uri? uri) ||
OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2181(Parameters.RequestUri),
uri: SR.FormatID8000(SR.ID2181));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the "expires_in"
/// parameter contained in the pushed authorization response.
/// </summary>
public sealed class ValidatePushedAuthorizationExpiration : IOpenIddictClientHandler<HandlePushedAuthorizationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandlePushedAuthorizationResponseContext>()
.UseSingletonHandler<ValidatePushedAuthorizationExpiration>()
.SetOrder(ValidatePushedAuthorizationRequestUri.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandlePushedAuthorizationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Return an error if the mandatory "expires_in" parameter is missing.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc9126#section-2.2.
if (context.Response.ExpiresIn is null)
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2180(Parameters.ExpiresIn),
uri: SR.FormatID8000(SR.ID2180));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting redirection requests and invoking the corresponding event handlers.
/// </summary>

3
src/OpenIddict.Client/OpenIddictClientHandlers.Device.cs

@ -21,7 +21,8 @@ public static partial class OpenIddictClientHandlers
*/
ValidateWellKnownParameters.Descriptor,
HandleErrorResponse.Descriptor,
ValidateVerificationEndpointUri.Descriptor
ValidateVerificationEndpointUri.Descriptor,
ValidateExpiration.Descriptor
]);
/// <summary>

203
src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs

@ -31,9 +31,11 @@ public static partial class OpenIddictClientHandlers
ExtractEndSessionEndpoint.Descriptor,
ExtractMtlsDeviceAuthorizationEndpoint.Descriptor,
ExtractMtlsIntrospectionEndpoint.Descriptor,
ExtractMtlsPushedAuthorizationEndpoint.Descriptor,
ExtractMtlsRevocationEndpoint.Descriptor,
ExtractMtlsTokenEndpoint.Descriptor,
ExtractMtlsUserInfoEndpoint.Descriptor,
ExtractPushedAuthorizationEndpoint.Descriptor,
ExtractRevocationEndpoint.Descriptor,
ExtractTokenEndpoint.Descriptor,
ExtractUserInfoEndpoint.Descriptor,
@ -44,8 +46,10 @@ public static partial class OpenIddictClientHandlers
ExtractScopes.Descriptor,
ExtractIssuerParameterRequirement.Descriptor,
ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor,
ExtractPushedAuthorizationRequirement.Descriptor,
ExtractDeviceAuthorizationEndpointClientAuthenticationMethods.Descriptor,
ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor,
ExtractPushedAuthorizationEndpointClientAuthenticationMethods.Descriptor,
ExtractRevocationEndpointClientAuthenticationMethods.Descriptor,
ExtractTokenEndpointClientAuthenticationMethods.Descriptor,
@ -109,29 +113,33 @@ public static partial class OpenIddictClientHandlers
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as unique strings:
Metadata.AuthorizationEndpoint or
Metadata.DeviceAuthorizationEndpoint or
Metadata.EndSessionEndpoint or
Metadata.Issuer or
Metadata.JwksUri or
Metadata.TokenEndpoint or
Metadata.AuthorizationEndpoint or
Metadata.DeviceAuthorizationEndpoint or
Metadata.EndSessionEndpoint or
Metadata.Issuer or
Metadata.JwksUri or
Metadata.PushedAuthorizationRequestEndpoint or
Metadata.TokenEndpoint or
Metadata.UserInfoEndpoint
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as arrays of strings:
Metadata.CodeChallengeMethodsSupported or
Metadata.DeviceAuthorizationEndpointAuthMethodsSupported or
Metadata.GrantTypesSupported or
Metadata.ResponseModesSupported or
Metadata.ResponseTypesSupported or
Metadata.ScopesSupported or
Metadata.CodeChallengeMethodsSupported or
Metadata.DeviceAuthorizationEndpointAuthMethodsSupported or
Metadata.GrantTypesSupported or
Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported or
Metadata.ResponseModesSupported or
Metadata.ResponseTypesSupported or
Metadata.ScopesSupported or
Metadata.TokenEndpointAuthMethodsSupported
=> ((JsonElement) value) is JsonElement element &&
element.ValueKind is JsonValueKind.Array &&
OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String),
// The following parameters MUST be formatted as booleans:
Metadata.AuthorizationResponseIssParameterSupported
Metadata.AuthorizationResponseIssParameterSupported or
Metadata.RequirePushedAuthorizationRequests or
Metadata.TlsClientCertificateBoundAccessTokens
=> ((JsonElement) value).ValueKind is JsonValueKind.True or JsonValueKind.False,
// Parameters that are not in the well-known list can be of any type.
@ -564,6 +572,47 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled pushed authorization endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsPushedAuthorizationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsPushedAuthorizationEndpoint>()
.SetOrder(ExtractMtlsIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.PushedAuthorizationRequestEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsPushedAuthorizationEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled revocation endpoint URI from the discovery document.
/// </summary>
@ -575,7 +624,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsRevocationEndpoint>()
.SetOrder(ExtractMtlsIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetOrder(ExtractMtlsPushedAuthorizationEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -687,6 +736,49 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting the pushed authorization endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractPushedAuthorizationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractPushedAuthorizationEndpoint>()
.SetOrder(ExtractEndSessionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var endpoint = (string?) context.Response[Metadata.PushedAuthorizationRequestEndpoint];
if (!string.IsNullOrEmpty(endpoint))
{
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2100(Metadata.PushedAuthorizationRequestEndpoint),
uri: SR.FormatID8000(SR.ID2100));
return default;
}
context.Configuration.PushedAuthorizationEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the revocation endpoint URI from the discovery document.
/// </summary>
@ -698,7 +790,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractRevocationEndpoint>()
.SetOrder(ExtractEndSessionEndpoint.Descriptor.Order + 1_000)
.SetOrder(ExtractPushedAuthorizationEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -1088,6 +1180,37 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting the flag indicating whether pushed
/// authorization requests (PAR) are considered mandatory from the discovery document.
/// </summary>
public sealed class ExtractPushedAuthorizationRequirement : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractPushedAuthorizationRequirement>()
.SetOrder(ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Configuration.RequirePushedAuthorizationRequests = (bool?)
context.Response[Metadata.RequirePushedAuthorizationRequests];
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the authentication methods
/// supported by the device authorization endpoint from the discovery document.
@ -1100,7 +1223,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractDeviceAuthorizationEndpointClientAuthenticationMethods>()
.SetOrder(ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor.Order + 1_000)
.SetOrder(ExtractPushedAuthorizationRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -1177,6 +1300,52 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting the authentication methods
/// supported by the pushed authorization endpoint from the discovery document.
/// </summary>
public sealed class ExtractPushedAuthorizationEndpointClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractPushedAuthorizationEndpointClientAuthenticationMethods>()
.SetOrder(ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Resolve the client authentication methods supported by the pushed authorization endpoint, if available.
//
// Note: "pushed_authorization_request_endpoint_auth_methods_supported" is not a standard parameter
// but is supported by OpenIddict 6.1.0 and higher for consistency with the other endpoints.
var methods = context.Response[Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported]?.GetUnnamedParameters();
if (methods is { Count: > 0 })
{
for (var index = 0; index < methods.Count; index++)
{
// Note: custom values are allowed in this case.
var method = (string?) methods[index];
if (!string.IsNullOrEmpty(method))
{
context.Configuration.PushedAuthorizationEndpointAuthMethodsSupported.Add(method);
}
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the authentication methods
/// supported by the revocation endpoint from the discovery document.
@ -1189,7 +1358,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractRevocationEndpointClientAuthenticationMethods>()
.SetOrder(ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor.Order + 1_000)
.SetOrder(ExtractPushedAuthorizationEndpointClientAuthenticationMethods.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

549
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -120,13 +120,21 @@ public static partial class OpenIddictClientHandlers
GenerateLoginStateToken.Descriptor,
AttachChallengeParameters.Descriptor,
AttachCustomChallengeParameters.Descriptor,
EvaluateDeviceAuthorizationRequest.Descriptor,
AttachDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor,
ResolveDeviceAuthorizationEndpoint.Descriptor,
AttachDeviceAuthorizationRequestParameters.Descriptor,
EvaluatePushedAuthorizationRequest.Descriptor,
AttachPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor,
ResolvePushedAuthorizationEndpoint.Descriptor,
AttachPushedAuthorizationRequestParameters.Descriptor,
EvaluateGeneratedChallengeClientAssertion.Descriptor,
PrepareChallengeClientAssertionPrincipal.Descriptor,
GenerateChallengeClientAssertion.Descriptor,
AttachDeviceAuthorizationRequestClientCredentials.Descriptor,
SendDeviceAuthorizationRequest.Descriptor,
@ -134,6 +142,17 @@ public static partial class OpenIddictClientHandlers
ResolveValidatedDeviceAuthorizationTokens.Descriptor,
ValidateRequiredDeviceAuthorizationTokens.Descriptor,
AttachPushedAuthorizationRequestClientCredentials.Descriptor,
ValidatePushedAuthorizationRequirement.Descriptor,
SendPushedAuthorizationRequest.Descriptor,
EvaluateValidatedPushedTokens.Descriptor,
ResolveValidatedPushedTokens.Descriptor,
ValidateRequiredPushedAuthorizationTokens.Descriptor,
AttachRequestToken.Descriptor,
RemovePushedAuthorizationRequestParameters.Descriptor,
/*
* Introspection processing:
*/
@ -880,7 +899,7 @@ public static partial class OpenIddictClientHandlers
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2142),
description: SR.FormatID2142(Parameters.State),
uri: SR.FormatID8000(SR.ID2142));
return default;
@ -5737,6 +5756,205 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for determining whether a pushed authorization request should be sent.
/// </summary>
public sealed class EvaluatePushedAuthorizationRequest : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<EvaluatePushedAuthorizationRequest>()
.SetOrder(AttachDeviceAuthorizationRequestParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.SendPushedAuthorizationRequest = context.GrantType switch
{
// For interactive grant types, always send a pushed authorization request by default if
// the authorization endpoint exposes a pushed authorization request endpoint and pushed
// authorization requests were was not explicitly opted out in the client registration.
GrantTypes.AuthorizationCode or GrantTypes.Implicit
when context.Configuration.PushedAuthorizationEndpoint is not null &&
!context.Registration.DisablePushedAuthorizationRequests => true,
// Apply the same logic to the special response_type=none flow.
null when context.ResponseType is ResponseTypes.None &&
context.Configuration.PushedAuthorizationEndpoint is not null &&
!context.Registration.DisablePushedAuthorizationRequests => true,
// Otherwise, do not send a pushed authorization request.
_ => false
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for negotiating the best pushed authorization endpoint
/// client authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachPushedAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<AttachPushedAuthorizationEndpointClientAuthenticationMethod>()
.SetOrder(EvaluatePushedAuthorizationRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.PushedAuthorizationEndpointClientAuthenticationMethod))
{
return default;
}
context.PushedAuthorizationEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the client registration, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Registration.ClientAuthenticationMethods.Count switch
{
0 => context.Options.ClientAuthenticationMethods as ICollection<string>,
_ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
},
// Note: if the authorization server doesn't support the OpenIddict-specific
// "pushed_authorization_request_endpoint_auth_methods_supported" node, fall back to
// the "token_endpoint_auth_methods_supported" node, as required by the specification.
//
// See https://datatracker.ietf.org/doc/html/rfc9126#section-2 for more information.
Server: context.Configuration.PushedAuthorizationEndpointAuthMethodsSupported.Count switch
{
0 => context.Configuration.TokenEndpointAuthMethodsSupported,
_ => context.Configuration.PushedAuthorizationEndpointAuthMethodsSupported,
}) switch
{
// If at least one signing key was attached to the client registration and both
// the client and the server explicitly support private_key_jwt, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when context.Registration.SigningCredentials.Count is not 0 &&
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the client registration and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
_ => null
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the URI of the pushed authorization endpoint.
/// </summary>
public sealed class ResolvePushedAuthorizationEndpoint : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<ResolvePushedAuthorizationEndpoint>()
.SetOrder(AttachPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If the URI of the pushed authorization endpoint endpoint wasn't
// explicitly set at this stage, try to extract it from the server configuration.
context.PushedAuthorizationEndpoint ??= context.PushedAuthorizationEndpointClientAuthenticationMethod switch
{
// When TLS client certificate authentication was negotiated,
// always favor the mTLS-specific endpoint if available.
ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth
when context.Configuration.MtlsPushedAuthorizationEndpoint is { IsAbsoluteUri: true } uri &&
!OpenIddictHelpers.IsImplicitFileUri(uri) => uri,
// Otherwise, use the non-mTLS-specific endpoint.
_ when context.Configuration.PushedAuthorizationEndpoint is { IsAbsoluteUri: true } uri &&
!OpenIddictHelpers.IsImplicitFileUri(uri) => uri,
_ => null
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters to the pushed authorization request, if applicable.
/// </summary>
public sealed class AttachPushedAuthorizationRequestParameters : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<AttachPushedAuthorizationRequestParameters>()
.SetOrder(ResolvePushedAuthorizationEndpoint.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Attach a new request instance if necessary.
context.PushedAuthorizationRequest ??= new OpenIddictRequest();
// Copy all the challenge parameters to the pushed authorization request instance.
foreach (var parameter in context.Request.GetParameters())
{
context.PushedAuthorizationRequest.AddParameter(parameter.Key, parameter.Value);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for selecting the token types that should
/// be generated and optionally sent as part of the challenge demand.
@ -5762,12 +5980,12 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
(context.GenerateClientAssertion,
context.IncludeClientAssertion) = context.DeviceAuthorizationEndpointClientAuthenticationMethod switch
(context.GenerateClientAssertion, context.IncludeClientAssertion) = context switch
{
// If the private_key_jwt client authentication method could be negotiated,
// generate a client assertion that will be used to authenticate the client.
ClientAuthenticationMethods.PrivateKeyJwt => (true, true),
{ DeviceAuthorizationEndpointClientAuthenticationMethod: ClientAuthenticationMethods.PrivateKeyJwt } => (true, true),
{ PushedAuthorizationEndpointClientAuthenticationMethod: ClientAuthenticationMethods.PrivateKeyJwt } => (true, true),
_ => (false, false)
};
@ -6148,6 +6366,329 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the pushed authorization endpoint request, if applicable.
/// </summary>
public sealed class AttachPushedAuthorizationRequestClientCredentials : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<AttachPushedAuthorizationRequestClientCredentials>()
.SetOrder(ValidateRequiredDeviceAuthorizationTokens.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.PushedAuthorizationRequest is not null, SR.GetResourceString(SR.ID4008));
// Always attach the client_id to the request, even if an assertion is sent or mTLS is used.
context.PushedAuthorizationRequest.ClientId = context.ClientId;
// Note: client authentication methods are mutually exclusive so the client_assertion
// and client_secret parameters MUST never be sent at the same time. For more information,
// see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.
if (context.IncludeClientAssertion)
{
context.PushedAuthorizationRequest.ClientAssertion = context.ClientAssertion;
context.PushedAuthorizationRequest.ClientAssertionType = context.ClientAssertionType;
}
else if (context.PushedAuthorizationEndpointClientAuthenticationMethod is
ClientAuthenticationMethods.ClientSecretBasic or
ClientAuthenticationMethods.ClientSecretPost)
{
context.PushedAuthorizationRequest.ClientSecret = context.Registration.ClientSecret;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for aborting authentication demands pointing to client registrations that
/// disallow using pushed authorization requests if the authorization server requires using this feature.
/// </summary>
public sealed class ValidatePushedAuthorizationRequirement : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<ValidatePushedAuthorizationRequirement>()
.SetOrder(AttachPushedAuthorizationRequestClientCredentials.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.SendPushedAuthorizationRequest && context.Configuration.RequirePushedAuthorizationRequests is true)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0460));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for sending the pushed authorization endpoint request, if applicable.
/// </summary>
public sealed class SendPushedAuthorizationRequest : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly OpenIddictClientService _service;
public SendPushedAuthorizationRequest(OpenIddictClientService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<SendPushedAuthorizationRequest>()
.SetOrder(ValidatePushedAuthorizationRequirement.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.PushedAuthorizationRequest is not null, SR.GetResourceString(SR.ID4008));
// Ensure the pushed authorization endpoint is present and is a valid absolute URI.
if (context.PushedAuthorizationEndpoint is not { IsAbsoluteUri: true } ||
OpenIddictHelpers.IsImplicitFileUri(context.PushedAuthorizationEndpoint))
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.PushedAuthorizationRequestEndpoint));
}
try
{
context.PushedAuthorizationResponse = await _service.SendPushedAuthorizationRequestAsync(
context.Registration, context.Configuration,
context.PushedAuthorizationRequest, context.PushedAuthorizationEndpoint,
context.PushedAuthorizationEndpointClientAuthenticationMethod, context.CancellationToken);
}
catch (ProtocolException exception)
{
context.Reject(
error: exception.Error,
description: exception.ErrorDescription,
uri: exception.ErrorUri);
return;
}
}
}
/// <summary>
/// Contains the logic responsible for determining the set of pushed authorization tokens to validate.
/// </summary>
public sealed class EvaluateValidatedPushedTokens : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<EvaluateValidatedPushedTokens>()
.SetOrder(SendPushedAuthorizationRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
(context.ExtractRequestToken,
context.RequireRequestToken,
context.ValidateRequestToken,
context.RejectRequestToken) = context.SendPushedAuthorizationRequest switch
{
// A request_uri parameter (whose content is called a request token in
// OpenIddict) is always returned as part of pushed authorization responses.
//
// Note: since request tokens are supposed to be opaque to the clients,
// they are never validated by default. Clients that need to deal with
// non-standard implementations can use custom handlers to validate
// request tokens that use a readable format (e.g JWT).
true => (true, true, false, false),
_ => (false, false, false, false)
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the pushed
/// tokens from the pushed authorization response, if applicable.
/// </summary>
public sealed class ResolveValidatedPushedTokens : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<ResolveValidatedPushedTokens>()
.SetOrder(EvaluateValidatedPushedTokens.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.PushedAuthorizationResponse is not null, SR.GetResourceString(SR.ID4007));
context.RequestToken = context.ExtractRequestToken ? context.PushedAuthorizationResponse.RequestUri : null;
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting challenge demands that lack required tokens.
/// </summary>
public sealed class ValidateRequiredPushedAuthorizationTokens : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<ValidateRequiredPushedAuthorizationTokens>()
// Note: this handler is registered with a high gap to allow handlers
// that do token extraction to be executed before this handler runs.
.SetOrder(ResolveValidatedPushedTokens.Descriptor.Order + 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.RequireRequestToken && string.IsNullOrEmpty(context.RequestToken))
{
context.Reject(
error: Errors.MissingToken,
description: SR.GetResourceString(SR.ID2000),
uri: SR.FormatID8000(SR.ID2000));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the request token to the authorization request.
/// </summary>
public sealed class AttachRequestToken : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachRequestToken>()
.SetOrder(ValidateRequiredPushedAuthorizationTokens.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.RequestUri = context.RequestToken;
return default;
}
}
/// <summary>
/// Contains the logic responsible for removing parameters that were sent as
/// part of the pushed authorization request from the authorization request.
/// </summary>
public sealed class RemovePushedAuthorizationRequestParameters : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.AddFilter<RequirePushedAuthorizationRequest>()
.UseSingletonHandler<RemovePushedAuthorizationRequestParameters>()
.SetOrder(AttachRequestToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.PushedAuthorizationRequest is not null, SR.GetResourceString(SR.ID4008));
// Filter out all the parameters that were sent in the pushed authorization request from
// the regular authorization request, except the "client_id" parameter, as required
// by the specification: https://datatracker.ietf.org/doc/html/rfc9101#section-5.
context.Request = new OpenIddictRequest(
from parameter in context.Request.GetParameters()
where parameter.Key is Parameters.ClientId || !context.PushedAuthorizationRequest.HasParameter(parameter.Key)
select parameter);
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting invalid introspection demands.
/// </summary>

9
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -32,6 +32,15 @@ public sealed class OpenIddictClientRegistration
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether pushed authorization requests are disabled.
/// When pushed authorization requests are disabled, PAR is not used by the OpenIddict client,
/// even if the remote authorization server exposes a pushed authorization endpoint. If the
/// authorization server requires using PAR but this property is set to <see langword="null"/>,
/// an exception is automatically thrown when starting an interactive authentication challenge.
/// </summary>
public bool DisablePushedAuthorizationRequests { get; set; }
/// <summary>
/// Gets or sets the URI of the redirection endpoint that will handle the callback.
/// </summary>

179
src/OpenIddict.Client/OpenIddictClientService.cs

@ -806,7 +806,8 @@ public class OpenIddictClientService
Issuer = request.Issuer,
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId
RegistrationId = request.RegistrationId,
Request = new()
};
if (request.Scopes is { Count: > 0 })
@ -1962,6 +1963,182 @@ public class OpenIddictClientService
}
}
/// <summary>
/// Sends the pushed authorization request and retrieves the corresponding response.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="configuration">The server configuration.</param>
/// <param name="request">The pushed authorization request.</param>
/// <param name="uri">The uri of the remote pushed authorization endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns>
internal async ValueTask<OpenIddictResponse> SendPushedAuthorizationRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (uri is null)
{
throw new ArgumentNullException(nameof(uri));
}
if (!uri.IsAbsoluteUri || OpenIddictHelpers.IsImplicitFileUri(uri))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
request = await PreparePushedAuthorizationRequestAsync();
request = await ApplyPushedAuthorizationRequestAsync();
var response = await ExtractPushedAuthorizationResponseAsync();
return await HandlePushedAuthorizationResponseAsync();
async ValueTask<OpenIddictRequest> PreparePushedAuthorizationRequestAsync()
{
var context = new PreparePushedAuthorizationRequestContext(transaction)
{
CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
Request = request
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0461(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
return context.Request;
}
async ValueTask<OpenIddictRequest> ApplyPushedAuthorizationRequestAsync()
{
var context = new ApplyPushedAuthorizationRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
Request = request
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0462(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6235), context.RemoteUri, context.Request);
return context.Request;
}
async ValueTask<OpenIddictResponse> ExtractPushedAuthorizationResponseAsync()
{
var context = new ExtractPushedAuthorizationResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
Request = request
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0463(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007));
context.Logger.LogInformation(SR.GetResourceString(SR.ID6236), context.RemoteUri, context.Response);
return context.Response;
}
async ValueTask<OpenIddictResponse> HandlePushedAuthorizationResponseAsync()
{
var context = new HandlePushedAuthorizationResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
Request = request,
Response = response
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0464(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
return context.Response;
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Sends the revocation request and retrieves the corresponding response.
/// </summary>

17
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using Microsoft.AspNetCore;
using Microsoft.Extensions.Caching.Distributed;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
namespace Microsoft.Extensions.DependencyInjection;
@ -127,16 +128,26 @@ public sealed class OpenIddictServerAspNetCoreBuilder
/// OpenID Connect authorization requests support is required.
/// </summary>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder EnableAuthorizationRequestCaching()
=> Configure(options => options.EnableAuthorizationRequestCaching = true);
{
Services.Configure<OpenIddictServerOptions>(options => options.EnableAuthorizationRequestCaching = true);
return this;
}
/// <summary>
/// Enables end session request caching, so that end session requests
/// are automatically stored in the distributed cache.
/// </summary>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder EnableEndSessionRequestCaching()
=> Configure(options => options.EnableEndSessionRequestCaching = true);
{
Services.Configure<OpenIddictServerOptions>(options => options.EnableEndSessionRequestCaching = true);
return this;
}
/// <summary>
/// Enables status code pages integration support. Once enabled, errors
@ -174,6 +185,7 @@ public sealed class OpenIddictServerAspNetCoreBuilder
/// </summary>
/// <param name="policy">The caching policy.</param>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder SetAuthorizationRequestCachingPolicy(DistributedCacheEntryOptions policy)
{
if (policy is null)
@ -190,6 +202,7 @@ public sealed class OpenIddictServerAspNetCoreBuilder
/// </summary>
/// <param name="policy">The caching policy.</param>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder SetEndSessionRequestCachingPolicy(DistributedCacheEntryOptions policy)
{
if (policy is null)

4
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs

@ -11,12 +11,14 @@ namespace OpenIddict.Server.AspNetCore;
/// </summary>
public static class OpenIddictServerAspNetCoreConstants
{
[Obsolete("This class is obsolete and will be removed in a future version.")]
public static class Cache
{
public const string AuthorizationRequest = "openiddict-authorization-request:";
public const string EndSessionRequest = "openiddict-end_session-request:";
}
[Obsolete("This class is obsolete and will be removed in a future version.")]
public static class JsonWebTokenTypes
{
public static class Private
@ -37,6 +39,7 @@ public static class OpenIddictServerAspNetCoreConstants
public const string ErrorUri = ".error_uri";
public const string IdentityTokenPrincipal = ".identity_token_principal";
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string RequestTokenPrincipal = ".request_token_principal";
public const string Scope = ".scope";
public const string UserCodePrincipal = ".user_code_principal";
}
@ -48,6 +51,7 @@ public static class OpenIddictServerAspNetCoreConstants
public const string ClientAssertion = "client_assertion";
public const string DeviceCode = "device_code";
public const string IdentityToken = "id_token";
public const string RequestToken = "request_token";
public const string RefreshToken = "refresh_token";
public const string UserCode = "user_code";
}

4
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs

@ -38,9 +38,11 @@ public static class OpenIddictServerAspNetCoreExtensions
builder.Services.TryAdd(OpenIddictServerAspNetCoreHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Register the built-in filters used by the default OpenIddict ASP.NET Core server event handlers.
#pragma warning disable CS0618
builder.Services.TryAddSingleton<RequireAuthorizationRequestCachingEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireEndSessionRequestCachingEnabled>();
#pragma warning restore CS0618
builder.Services.TryAddSingleton<RequireAuthorizationEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireEndSessionEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireErrorPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireHttpRequest>();

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

@ -178,12 +178,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
OpenIddictServerEndpointType.EndUserVerification => context.UserCodePrincipal,
OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation
=> context.AccessTokenPrincipal ??
context.RefreshTokenPrincipal ??
context.IdentityTokenPrincipal ??
context.AuthorizationCodePrincipal ??
context.DeviceCodePrincipal ??
context.UserCodePrincipal,
=> context.GenericTokenPrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.AuthorizationCodePrincipal,
@ -282,6 +277,16 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
});
}
if (!string.IsNullOrEmpty(context.RequestToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.RequestToken,
Value = context.RequestToken
});
}
if (!string.IsNullOrEmpty(context.UserCode))
{
tokens ??= new(capacity: 1);
@ -322,6 +327,11 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);
}
if (context.RequestTokenPrincipal is not null)
{
properties.SetParameter(Properties.RequestTokenPrincipal, context.RequestTokenPrincipal);
}
if (context.UserCodePrincipal is not null)
{
properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal);

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs

@ -19,6 +19,7 @@ public static class OpenIddictServerAspNetCoreHandlerFilters
/// <summary>
/// Represents a filter that excludes the associated handlers if authorization request caching was not enabled.
/// </summary>
[Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireAuthorizationRequestCachingEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;
@ -64,6 +65,7 @@ public static class OpenIddictServerAspNetCoreHandlerFilters
/// <summary>
/// Represents a filter that excludes the associated handlers if end session request caching was not enabled.
/// </summary>
[Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireEndSessionRequestCachingEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;

268
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs

@ -5,22 +5,14 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using OpenIddict.Extensions;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants;
using JsonWebTokenTypes = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.JsonWebTokenTypes;
namespace OpenIddict.Server.AspNetCore;
@ -33,8 +25,6 @@ public static partial class OpenIddictServerAspNetCoreHandlers
* Authorization request extraction:
*/
ExtractGetOrPostRequest<ExtractAuthorizationRequestContext>.Descriptor,
RestoreCachedRequestParameters.Descriptor,
CacheRequestParameters.Descriptor,
/*
* Authorization request handling:
@ -44,29 +34,43 @@ public static partial class OpenIddictServerAspNetCoreHandlers
/*
* Authorization response processing:
*/
RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyAuthorizationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyAuthorizationResponseContext>.Descriptor,
ProcessSelfRedirection.Descriptor,
ProcessFormPostResponse.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessFragmentResponse.Descriptor,
ProcessPassthroughErrorResponse<ApplyAuthorizationResponseContext, RequireAuthorizationEndpointPassthroughEnabled>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyAuthorizationResponseContext>.Descriptor,
ProcessLocalErrorResponse<ApplyAuthorizationResponseContext>.Descriptor
ProcessLocalErrorResponse<ApplyAuthorizationResponseContext>.Descriptor,
/*
* Pushed authorization request extraction:
*/
ExtractPostRequest<ExtractPushedAuthorizationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractPushedAuthorizationRequestContext>.Descriptor,
/*
* Pushed authorization response processing:
*/
AttachHttpResponseCode<ApplyPushedAuthorizationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyPushedAuthorizationResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyPushedAuthorizationResponseContext>.Descriptor,
ProcessJsonResponse<ApplyPushedAuthorizationResponseContext>.Descriptor
]);
/// <summary>
/// Contains the logic responsible for restoring cached requests from the request_id, if specified.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler<ExtractAuthorizationRequestContext>
{
private readonly IDistributedCache _cache;
public RestoreCachedRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RestoreCachedRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RestoreCachedRequestParameters(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -81,92 +85,23 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// If a request_id parameter can be found in the authorization request,
// restore the complete authorization request from the distributed cache.
if (string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
var token = await _cache.GetStringAsync(Cache.AuthorizationRequest + context.Request.RequestId);
if (token is null || !context.Options.JsonWebTokenHandler.CanReadToken(token))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6146), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = [JsonWebTokenTypes.Private.AuthorizationRequest];
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters);
if (!result.IsValid)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6146), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
using var document = JsonDocument.Parse(
Base64UrlEncoder.Decode(((JsonWebToken) result.SecurityToken).InnerToken.EncodedPayload));
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0117));
}
// Restore the request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
if (!context.Request.HasParameter(parameter.Name))
{
context.Request.AddParameter(parameter.Name, parameter.Value.Clone());
}
}
}
public ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for caching authorization requests, if applicable.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler<ExtractAuthorizationRequestContext>
{
private readonly IDistributedCache _cache;
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;
public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor<OpenIddictServerAspNetCoreOptions> options)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -181,97 +116,21 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// 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() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Don't cache the request if the request doesn't include any parameter.
// If a request_id parameter can be found in the authorization request,
// ignore the following logic to prevent an infinite redirect loop.
if (context.Request.Count is 0 || !string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Generate a 256-bit request identifier using a crypto-secure random number generator.
context.Request.RequestId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
// Build a list of claims matching the parameters extracted from the request.
//
// Note: in most cases, parameters should be representated as strings as requests are
// typically resolved from the query string or the request form, where parameters
// are natively represented as strings. However, requests can also be extracted from
// different places where they can be represented as complex JSON representations
// (e.g requests extracted from a JSON Web Token that may be encrypted and/or signed).
var claims = from parameter in context.Request.GetParameters()
let element = (JsonElement) parameter.Value
let type = element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.Number => ClaimValueTypes.Integer64,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
}
select new Claim(parameter.Key, element.ToString()!, type);
// Store the serialized authorization request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
await _cache.SetStringAsync(Cache.AuthorizationRequest + context.Request.RequestId,
token, _options.CurrentValue.AuthorizationRequestCachingPolicy);
// Create a new GET authorization request containing only the request_id parameter.
var location = QueryHelpers.AddQueryString(
uri: new UriBuilder(context.RequestUri) { Query = null }.Uri.AbsoluteUri,
name: Parameters.RequestId,
value: context.Request.RequestId);
request.HttpContext.Response.Redirect(location);
// Mark the response as handled to skip the rest of the pipeline.
context.HandleRequest();
}
public ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for removing cached authorization requests from the distributed cache.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler<ApplyAuthorizationResponseContext>
{
private readonly IDistributedCache _cache;
public RemoveCachedRequest() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RemoveCachedRequest() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RemoveCachedRequest(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -285,6 +144,28 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for processing authorization responses requiring a self-redirection.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public sealed class ProcessSelfRedirection : IOpenIddictServerHandler<ApplyAuthorizationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyAuthorizationResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessSelfRedirection>()
.SetOrder(250_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
{
@ -293,18 +174,47 @@ public static partial class OpenIddictServerAspNetCoreHandlers
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request?.RequestId))
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
if (string.IsNullOrEmpty(context.Response.RequestUri))
{
return default;
}
// Note: the ApplyAuthorizationResponse event is called for both successful
// and errored authorization responses but discrimination is not necessary here,
// as the authorization request must be removed from the distributed cache in both cases.
// 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
return new(_cache.RemoveAsync(Cache.AuthorizationRequest + context.Request.RequestId));
#if SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS
var location = QueryHelpers.AddQueryString(context.RequestUri.GetLeftPart(UriPartial.Path),
from parameter in context.Response.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select KeyValuePair.Create(parameter.Key, value));
#else
var location = context.RequestUri.GetLeftPart(UriPartial.Path);
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{
location = QueryHelpers.AddQueryString(location, key, value);
}
#endif
response.Redirect(location);
context.HandleRequest();
return default;
}
}
@ -326,7 +236,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyAuthorizationResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessFormPostResponse>()
.SetOrder(250_000)
.SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

253
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs

@ -5,19 +5,11 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants;
using JsonWebTokenTypes = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.JsonWebTokenTypes;
namespace OpenIddict.Server.AspNetCore;
@ -30,8 +22,6 @@ public static partial class OpenIddictServerAspNetCoreHandlers
* End-session request extraction:
*/
ExtractGetOrPostRequest<ExtractEndSessionRequestContext>.Descriptor,
RestoreCachedRequestParameters.Descriptor,
CacheRequestParameters.Descriptor,
/*
* End-session request handling:
@ -41,14 +31,14 @@ public static partial class OpenIddictServerAspNetCoreHandlers
/*
* End-session response processing:
*/
RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyEndSessionResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyEndSessionResponseContext>.Descriptor,
ProcessSelfRedirection.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse.Descriptor,
ProcessPassthroughErrorResponse<ApplyEndSessionResponseContext, RequireEndSessionEndpointPassthroughEnabled>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyEndSessionResponseContext>.Descriptor,
ProcessLocalErrorResponse<ApplyEndSessionResponseContext>.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessEmptyResponse<ApplyEndSessionResponseContext>.Descriptor
]);
@ -56,14 +46,13 @@ public static partial class OpenIddictServerAspNetCoreHandlers
/// Contains the logic responsible for restoring cached requests from the request_id, if specified.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler<ExtractEndSessionRequestContext>
{
private readonly IDistributedCache _cache;
public RestoreCachedRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RestoreCachedRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RestoreCachedRequestParameters(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -78,92 +67,23 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractEndSessionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// If a request_id parameter can be found in the end session request,
// restore the complete end session request from the distributed cache.
if (string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
var token = await _cache.GetStringAsync(Cache.EndSessionRequest + context.Request.RequestId);
if (token is null || !context.Options.JsonWebTokenHandler.CanReadToken(token))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6150), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = [JsonWebTokenTypes.Private.EndSessionRequest];
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters);
if (!result.IsValid)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6150), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
using var document = JsonDocument.Parse(
Base64UrlEncoder.Decode(((JsonWebToken) result.SecurityToken).InnerToken.EncodedPayload));
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0118));
}
// Restore the request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
if (!context.Request.HasParameter(parameter.Name))
{
context.Request.AddParameter(parameter.Name, parameter.Value.Clone());
}
}
}
public ValueTask HandleAsync(ExtractEndSessionRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for caching end session requests, if applicable.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler<ExtractEndSessionRequestContext>
{
private readonly IDistributedCache _cache;
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;
public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor<OpenIddictServerAspNetCoreOptions> options)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -178,97 +98,21 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractEndSessionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// 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() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Don't cache the request if the request doesn't include any parameter.
// If a request_id parameter can be found in the end session request,
// ignore the following logic to prevent an infinite redirect loop.
if (context.Request.Count is 0 || !string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Generate a 256-bit request identifier using a crypto-secure random number generator.
context.Request.RequestId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
// Build a list of claims matching the parameters extracted from the request.
//
// Note: in most cases, parameters should be representated as strings as requests are
// typically resolved from the query string or the request form, where parameters
// are natively represented as strings. However, requests can also be extracted from
// different places where they can be represented as complex JSON representations
// (e.g requests extracted from a JSON Web Token that may be encrypted and/or signed).
var claims = from parameter in context.Request.GetParameters()
let element = (JsonElement) parameter.Value
let type = element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.Number => ClaimValueTypes.Integer64,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
}
select new Claim(parameter.Key, element.ToString()!, type);
// Store the serialized end session request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.EndSessionRequest
});
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
await _cache.SetStringAsync(Cache.EndSessionRequest + context.Request.RequestId,
token, _options.CurrentValue.EndSessionRequestCachingPolicy);
// Create a new GET end session request containing only the request_id parameter.
var location = QueryHelpers.AddQueryString(
uri: new UriBuilder(context.RequestUri) { Query = null }.Uri.AbsoluteUri,
name: Parameters.RequestId,
value: context.Request.RequestId);
request.HttpContext.Response.Redirect(location);
// Mark the response as handled to skip the rest of the pipeline.
context.HandleRequest();
}
public ValueTask HandleAsync(ExtractEndSessionRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for removing cached end session requests from the distributed cache.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler<ApplyEndSessionResponseContext>
{
private readonly IDistributedCache _cache;
public RemoveCachedRequest() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RemoveCachedRequest() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RemoveCachedRequest(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -282,6 +126,28 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyEndSessionResponseContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for processing end session responses requiring a self-redirection.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public sealed class ProcessSelfRedirection : IOpenIddictServerHandler<ApplyEndSessionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyEndSessionResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessSelfRedirection>()
.SetOrder(250_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyEndSessionResponseContext context)
{
@ -290,18 +156,47 @@ public static partial class OpenIddictServerAspNetCoreHandlers
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request?.RequestId))
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
if (string.IsNullOrEmpty(context.Response.RequestUri))
{
return default;
}
// Note: the ApplyEndSessionResponse event is called for both successful
// and errored end session responses but discrimination is not necessary here,
// as the end session request must be removed from the distributed cache in both cases.
// 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
#if SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS
var location = QueryHelpers.AddQueryString(context.RequestUri.GetLeftPart(UriPartial.Path),
from parameter in context.Response.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select KeyValuePair.Create(parameter.Key, value));
#else
var location = context.RequestUri.GetLeftPart(UriPartial.Path);
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
return new(_cache.RemoveAsync(Cache.EndSessionRequest + context.Request.RequestId));
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{
location = QueryHelpers.AddQueryString(location, key, value);
}
#endif
response.Redirect(location);
context.HandleRequest();
return default;
}
}
@ -318,7 +213,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyEndSessionResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessQueryResponse>()
.SetOrder(250_000)
.SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

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

@ -886,8 +886,27 @@ public static partial class OpenIddictServerAspNetCoreHandlers
throw new ArgumentNullException(nameof(context));
}
context.SkipRequest();
switch (context.EndpointType)
{
// When authorization request caching is enabled and the request doesn't contain a
// request_uri yet, do not enable the pass-through mode to allow OpenIddict to trigger
// a sign-in operation that will generate and attach a request token to the parameters.
case OpenIddictServerEndpointType.Authorization when
context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Transaction.Request?.RequestUri):
return default;
// When end session request caching is enabled and the request doesn't contain a
// request_uri yet, do not enable the pass-through mode to allow OpenIddict to trigger
// a sign-in operation that will generate and attach a request token to the parameters.
case OpenIddictServerEndpointType.EndSession when
context.Options.EnableEndSessionRequestCaching &&
string.IsNullOrEmpty(context.Transaction.Request?.RequestUri):
return default;
}
context.SkipRequest();
return default;
}
}
@ -926,6 +945,10 @@ public static partial class OpenIddictServerAspNetCoreHandlers
response.StatusCode = (context.EndpointType, context.Transaction.Response.Error) switch
{
// Note: for pushed authorization responses, the returned HTTP status code MUST be 201.
// See https://datatracker.ietf.org/doc/html/rfc9126#section-2.2 for more information.
(OpenIddictServerEndpointType.PushedAuthorization, null or { Length: 0 }) => 201,
// Note: the default code may be replaced by another handler (e.g when doing redirects).
(_, null or { Length: 0 }) => 200,

4
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs

@ -78,12 +78,14 @@ public sealed class OpenIddictServerAspNetCoreOptions : AuthenticationSchemeOpti
/// Enabling this option is recommended when using external authentication providers
/// or when large GET or POST OpenID Connect authorization requests support is required.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableAuthorizationRequestCaching { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether requests received by the end session endpoint should be cached.
/// When enabled, authorization requests are automatically stored in the distributed cache.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableEndSessionRequestCaching { get; set; }
/// <summary>
@ -106,6 +108,7 @@ public sealed class OpenIddictServerAspNetCoreOptions : AuthenticationSchemeOpti
/// <summary>
/// Gets or sets the caching policy used by the authorization endpoint.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public DistributedCacheEntryOptions AuthorizationRequestCachingPolicy { get; set; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
@ -115,6 +118,7 @@ public sealed class OpenIddictServerAspNetCoreOptions : AuthenticationSchemeOpti
/// <summary>
/// Gets or sets the caching policy used by the end session endpoint.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public DistributedCacheEntryOptions EndSessionRequestCachingPolicy { get; set; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),

7
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs

@ -99,6 +99,13 @@ public sealed class OpenIddictServerDataProtectionBuilder
public OpenIddictServerDataProtectionBuilder PreferDefaultDeviceCodeFormat()
=> Configure(options => options.PreferDefaultDeviceCodeFormat = true);
/// <summary>
/// Configures OpenIddict to use the default token format (JWT) when issuing new request tokens.
/// </summary>
/// <returns>The <see cref="OpenIddictServerDataProtectionBuilder"/> instance.</returns>
public OpenIddictServerDataProtectionBuilder PreferDefaultRequestTokenFormat()
=> Configure(options => options.PreferDefaultRequestTokenFormat = true);
/// <summary>
/// Configures OpenIddict to use the default token format (JWT) when issuing new refresh tokens.
/// </summary>

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

@ -27,6 +27,7 @@ public static class OpenIddictServerDataProtectionConstants
public const string OriginalRedirectUri = ".original_redirect_uri";
public const string Presenters = ".presenters";
public const string RefreshTokenLifetime = ".refresh_token_lifetime";
public const string RequestTokenLifetime = ".request_token_lifetime";
public const string Resources = ".resources";
public const string Scopes = ".scopes";
public const string UserCodeLifetime = ".user_code_lifetime";
@ -44,6 +45,7 @@ public static class OpenIddictServerDataProtectionConstants
public const string AccessToken = "AccessTokenFormat";
public const string AuthorizationCode = "AuthorizationCodeFormat";
public const string DeviceCode = "DeviceCodeFormat";
public const string RequestToken = "RequestTokenFormat";
public const string RefreshToken = "RefreshTokenFormat";
public const string UserCode = "UserCodeFormat";
}

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

@ -37,6 +37,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD
.SetClaim(Claims.Private.DeviceCodeLifetime, GetIntegerProperty(properties, Properties.DeviceCodeLifetime))
.SetClaim(Claims.Private.IdentityTokenLifetime, GetIntegerProperty(properties, Properties.IdentityTokenLifetime))
.SetClaim(Claims.Private.RefreshTokenLifetime, GetIntegerProperty(properties, Properties.RefreshTokenLifetime))
.SetClaim(Claims.Private.RequestTokenLifetime, GetIntegerProperty(properties, Properties.RequestTokenLifetime))
.SetClaim(Claims.Private.UserCodeLifetime, GetIntegerProperty(properties, Properties.UserCodeLifetime))
.SetClaims(Claims.Private.Audience, GetJsonProperty(properties, Properties.Audiences))
@ -214,6 +215,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD
SetProperty(properties, Properties.DeviceCodeLifetime, principal.GetClaim(Claims.Private.DeviceCodeLifetime));
SetProperty(properties, Properties.IdentityTokenLifetime, principal.GetClaim(Claims.Private.IdentityTokenLifetime));
SetProperty(properties, Properties.RefreshTokenLifetime, principal.GetClaim(Claims.Private.RefreshTokenLifetime));
SetProperty(properties, Properties.RequestTokenLifetime, principal.GetClaim(Claims.Private.RequestTokenLifetime));
SetProperty(properties, Properties.UserCodeLifetime, principal.GetClaim(Claims.Private.UserCodeLifetime));
SetProperty(properties, Properties.CodeChallenge, principal.GetClaim(Claims.Private.CodeChallenge));
@ -249,6 +251,7 @@ public sealed class OpenIddictServerDataProtectionFormatter : IOpenIddictServerD
Claims.Private.Presenter or
Claims.Private.RedirectUri or
Claims.Private.RefreshTokenLifetime or
Claims.Private.RequestTokenLifetime or
Claims.Private.Resource or
Claims.Private.Scope or
Claims.Private.TokenId or

42
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs

@ -113,35 +113,48 @@ public static partial class OpenIddictServerDataProtectionHandlers
ValidateToken(TokenTypeHints.AccessToken) ??
ValidateToken(TokenTypeHints.RefreshToken) ??
ValidateToken(TokenTypeHints.DeviceCode) ??
ValidateToken(TokenTypeHints.UserCode),
ValidateToken(TokenTypeHints.UserCode) ??
ValidateToken(TokenTypeHints.Private.RequestToken),
TokenTypeHints.DeviceCode =>
ValidateToken(TokenTypeHints.DeviceCode) ??
ValidateToken(TokenTypeHints.AccessToken) ??
ValidateToken(TokenTypeHints.RefreshToken) ??
ValidateToken(TokenTypeHints.AuthorizationCode) ??
ValidateToken(TokenTypeHints.UserCode),
ValidateToken(TokenTypeHints.UserCode) ??
ValidateToken(TokenTypeHints.Private.RequestToken),
TokenTypeHints.RefreshToken =>
ValidateToken(TokenTypeHints.RefreshToken) ??
ValidateToken(TokenTypeHints.AccessToken) ??
ValidateToken(TokenTypeHints.AuthorizationCode) ??
ValidateToken(TokenTypeHints.DeviceCode) ??
ValidateToken(TokenTypeHints.UserCode),
ValidateToken(TokenTypeHints.UserCode) ??
ValidateToken(TokenTypeHints.Private.RequestToken),
TokenTypeHints.UserCode =>
ValidateToken(TokenTypeHints.UserCode) ??
ValidateToken(TokenTypeHints.AccessToken) ??
ValidateToken(TokenTypeHints.RefreshToken) ??
ValidateToken(TokenTypeHints.AuthorizationCode) ??
ValidateToken(TokenTypeHints.DeviceCode),
ValidateToken(TokenTypeHints.DeviceCode) ??
ValidateToken(TokenTypeHints.Private.RequestToken),
TokenTypeHints.Private.RequestToken =>
ValidateToken(TokenTypeHints.AccessToken) ??
ValidateToken(TokenTypeHints.RefreshToken) ??
ValidateToken(TokenTypeHints.AuthorizationCode) ??
ValidateToken(TokenTypeHints.DeviceCode) ??
ValidateToken(TokenTypeHints.UserCode) ??
ValidateToken(TokenTypeHints.Private.RequestToken),
_ =>
ValidateToken(TokenTypeHints.AccessToken) ??
ValidateToken(TokenTypeHints.RefreshToken) ??
ValidateToken(TokenTypeHints.AuthorizationCode) ??
ValidateToken(TokenTypeHints.DeviceCode) ??
ValidateToken(TokenTypeHints.UserCode),
ValidateToken(TokenTypeHints.UserCode) ??
ValidateToken(TokenTypeHints.Private.RequestToken),
},
// If a single valid token type was set, ignore the specified token type hint.
@ -153,6 +166,8 @@ public static partial class OpenIddictServerDataProtectionHandlers
TokenTypeHints.DeviceCode => ValidateToken(TokenTypeHints.DeviceCode),
TokenTypeHints.UserCode => ValidateToken(TokenTypeHints.UserCode),
TokenTypeHints.Private.RequestToken => ValidateToken(TokenTypeHints.Private.RequestToken),
_ => null // The token type is not supported by the Data Protection integration (e.g identity tokens).
},
@ -169,6 +184,8 @@ public static partial class OpenIddictServerDataProtectionHandlers
TokenTypeHints.DeviceCode => 4,
TokenTypeHints.UserCode => 5,
TokenTypeHints.Private.RequestToken => 6,
_ => int.MaxValue
})
.Select(type => type switch
@ -179,6 +196,8 @@ public static partial class OpenIddictServerDataProtectionHandlers
TokenTypeHints.DeviceCode => ValidateToken(TokenTypeHints.DeviceCode),
TokenTypeHints.UserCode => ValidateToken(TokenTypeHints.UserCode),
TokenTypeHints.Private.RequestToken => ValidateToken(TokenTypeHints.Private.RequestToken),
_ => null // The token type is not supported by the Data Protection integration (e.g identity tokens).
})
.Where(static principal => principal is not null)
@ -234,6 +253,11 @@ public static partial class OpenIddictServerDataProtectionHandlers
(TokenTypeHints.UserCode, false)
=> [Handlers.Server, Formats.UserCode, Schemes.Server],
(TokenTypeHints.Private.RequestToken, true)
=> [Handlers.Server, Formats.RequestToken, Features.ReferenceTokens, Schemes.Server],
(TokenTypeHints.Private.RequestToken, false)
=> [Handlers.Server, Formats.RequestToken, Schemes.Server],
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
});
@ -309,6 +333,9 @@ public static partial class OpenIddictServerDataProtectionHandlers
TokenTypeHints.UserCode when !_options.CurrentValue.PreferDefaultUserCodeFormat
=> TokenFormats.Private.DataProtection,
TokenTypeHints.Private.RequestToken when !_options.CurrentValue.PreferDefaultRequestTokenFormat
=> TokenFormats.Private.DataProtection,
_ => context.TokenFormat // Don't override the format if the token type is not supported.
};
@ -382,6 +409,11 @@ public static partial class OpenIddictServerDataProtectionHandlers
(TokenTypeHints.UserCode, false)
=> [Handlers.Server, Formats.UserCode, Schemes.Server],
(TokenTypeHints.Private.RequestToken, true)
=> [Handlers.Server, Formats.RequestToken, Features.ReferenceTokens, Schemes.Server],
(TokenTypeHints.Private.RequestToken, false)
=> [Handlers.Server, Formats.RequestToken, Schemes.Server],
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
});

6
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs

@ -46,6 +46,12 @@ public sealed class OpenIddictServerDataProtectionOptions
/// </summary>
public bool PreferDefaultDeviceCodeFormat { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the default user code format should be used when issuing
/// new request tokens. This property is set to <see langword="false"/> by default.
/// </summary>
public bool PreferDefaultRequestTokenFormat { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the default refresh token format should be
/// used when issuing new refresh tokens. This property is set to <see langword="false"/> by default.

17
src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs

@ -6,6 +6,7 @@
using System.ComponentModel;
using Microsoft.Extensions.Caching.Distributed;
using OpenIddict.Server;
using OpenIddict.Server.Owin;
using Owin;
@ -124,16 +125,26 @@ public sealed class OpenIddictServerOwinBuilder
/// OpenID Connect authorization requests support is required.
/// </summary>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder EnableAuthorizationRequestCaching()
=> Configure(options => options.EnableAuthorizationRequestCaching = true);
{
Services.Configure<OpenIddictServerOptions>(options => options.EnableAuthorizationRequestCaching = true);
return this;
}
/// <summary>
/// Enables end session request caching, so that end session requests
/// are automatically stored in the distributed cache.
/// </summary>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder EnableEndSessionRequestCaching()
=> Configure(options => options.EnableEndSessionRequestCaching = true);
{
Services.Configure<OpenIddictServerOptions>(options => options.EnableEndSessionRequestCaching = true);
return this;
}
/// <summary>
/// Suppresses indentation for the JSON responses returned by the OWIN host.
@ -163,6 +174,7 @@ public sealed class OpenIddictServerOwinBuilder
/// </summary>
/// <param name="policy">The caching policy.</param>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder SetAuthorizationRequestCachingPolicy(DistributedCacheEntryOptions policy)
{
if (policy is null)
@ -179,6 +191,7 @@ public sealed class OpenIddictServerOwinBuilder
/// </summary>
/// <param name="policy">The caching policy.</param>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/> instance.</returns>
[Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder SetEndSessionRequestCachingPolicy(DistributedCacheEntryOptions policy)
{
if (policy is null)

3
src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs

@ -11,6 +11,7 @@ namespace OpenIddict.Server.Owin;
/// </summary>
public static class OpenIddictServerOwinConstants
{
[Obsolete("This class is obsolete and will be removed in a future version.")]
public static class Cache
{
public const string AuthorizationRequest = "openiddict-authorization-request:";
@ -28,6 +29,7 @@ public static class OpenIddictServerOwinConstants
public const string WwwAuthenticate = "WWW-Authenticate";
}
[Obsolete("This class is obsolete and will be removed in a future version.")]
public static class JsonWebTokenTypes
{
public static class Private
@ -67,6 +69,7 @@ public static class OpenIddictServerOwinConstants
public const string DeviceCode = "device_code";
public const string IdentityToken = "id_token";
public const string RefreshToken = "refresh_token";
public const string RequestToken = "request_token";
public const string UserCode = "user_code";
}
}

4
src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs

@ -41,9 +41,11 @@ public static class OpenIddictServerOwinExtensions
builder.Services.TryAdd(OpenIddictServerOwinHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
// Register the built-in filters used by the default OpenIddict OWIN server event handlers.
#pragma warning disable CS0618
builder.Services.TryAddSingleton<RequireAuthorizationRequestCachingEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireEndSessionRequestCachingEnabled>();
#pragma warning restore CS0618
builder.Services.TryAddSingleton<RequireAuthorizationEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireEndSessionEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireErrorPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireTransportSecurityRequirementEnabled>();

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

@ -162,6 +162,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
else
{
// A single main claims-based principal instance can be attached to an authentication ticket.
// To return the most appropriate one, the principal is selected based on the endpoint type.
var principal = context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.EndSession
@ -170,12 +171,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
OpenIddictServerEndpointType.EndUserVerification => context.UserCodePrincipal,
OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation
=> context.AccessTokenPrincipal ??
context.RefreshTokenPrincipal ??
context.IdentityTokenPrincipal ??
context.AuthorizationCodePrincipal ??
context.DeviceCodePrincipal ??
context.UserCodePrincipal,
=> context.GenericTokenPrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.AuthorizationCodePrincipal,
@ -240,6 +236,11 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
properties.Dictionary[Tokens.RefreshToken] = context.RefreshToken;
}
if (!string.IsNullOrEmpty(context.RequestToken))
{
properties.Dictionary[Tokens.RequestToken] = context.RequestToken;
}
if (!string.IsNullOrEmpty(context.UserCode))
{
properties.Dictionary[Tokens.UserCode] = context.UserCode;

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs

@ -17,6 +17,7 @@ public static class OpenIddictServerOwinHandlerFilters
/// <summary>
/// Represents a filter that excludes the associated handlers if authorization request caching was not enabled.
/// </summary>
[Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireAuthorizationRequestCachingEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;
@ -62,6 +63,7 @@ public static class OpenIddictServerOwinHandlerFilters
/// <summary>
/// Represents a filter that excludes the associated handlers if end session request caching was not enabled.
/// </summary>
[Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireEndSessionRequestCachingEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;

259
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs

@ -5,20 +5,13 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using Owin;
using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants;
using JsonWebTokenTypes = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.JsonWebTokenTypes;
namespace OpenIddict.Server.Owin;
@ -31,8 +24,6 @@ public static partial class OpenIddictServerOwinHandlers
* Authorization request extraction:
*/
ExtractGetOrPostRequest<ExtractAuthorizationRequestContext>.Descriptor,
RestoreCachedRequestParameters.Descriptor,
CacheRequestParameters.Descriptor,
/*
* Authorization request handling:
@ -42,30 +33,46 @@ public static partial class OpenIddictServerOwinHandlers
/*
* Authorization response processing:
*/
RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyAuthorizationResponseContext>.Descriptor,
AttachOwinResponseChallenge<ApplyAuthorizationResponseContext>.Descriptor,
SuppressFormsAuthenticationRedirect<ApplyAuthorizationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyAuthorizationResponseContext>.Descriptor,
ProcessSelfRedirection.Descriptor,
ProcessFormPostResponse.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessFragmentResponse.Descriptor,
ProcessPassthroughErrorResponse<ApplyAuthorizationResponseContext, RequireAuthorizationEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyAuthorizationResponseContext>.Descriptor
ProcessLocalErrorResponse<ApplyAuthorizationResponseContext>.Descriptor,
/*
* Pushed authorization request extraction:
*/
ExtractPostRequest<ExtractPushedAuthorizationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractPushedAuthorizationRequestContext>.Descriptor,
/*
* Pushed authorization response processing:
*/
AttachHttpResponseCode<ApplyPushedAuthorizationResponseContext>.Descriptor,
AttachOwinResponseChallenge<ApplyPushedAuthorizationResponseContext>.Descriptor,
SuppressFormsAuthenticationRedirect<ApplyPushedAuthorizationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyPushedAuthorizationResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyPushedAuthorizationResponseContext>.Descriptor,
ProcessJsonResponse<ApplyPushedAuthorizationResponseContext>.Descriptor,
]);
/// <summary>
/// Contains the logic responsible for restoring cached requests from the request_id, if specified.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler<ExtractAuthorizationRequestContext>
{
private readonly IDistributedCache _cache;
public RestoreCachedRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RestoreCachedRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RestoreCachedRequestParameters(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -80,92 +87,23 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// If a request_id parameter can be found in the authorization request,
// restore the complete authorization request from the distributed cache.
if (string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
var token = await _cache.GetStringAsync(Cache.AuthorizationRequest + context.Request.RequestId);
if (token is null || !context.Options.JsonWebTokenHandler.CanReadToken(token))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6146), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = [JsonWebTokenTypes.Private.AuthorizationRequest];
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters);
if (!result.IsValid)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6146), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
using var document = JsonDocument.Parse(
Base64UrlEncoder.Decode(((JsonWebToken) result.SecurityToken).InnerToken.EncodedPayload));
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0117));
}
// Restore the request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
if (!context.Request.HasParameter(parameter.Name))
{
context.Request.AddParameter(parameter.Name, parameter.Value.Clone());
}
}
}
public ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for caching authorization requests, if applicable.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler<ExtractAuthorizationRequestContext>
{
private readonly IDistributedCache _cache;
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;
public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor<OpenIddictServerOwinOptions> options)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -180,97 +118,21 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// 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() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Don't cache the request if the request doesn't include any parameter.
// If a request_id parameter can be found in the authorization request,
// ignore the following logic to prevent an infinite redirect loop.
if (context.Request.Count is 0 || !string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Generate a 256-bit request identifier using a crypto-secure random number generator.
context.Request.RequestId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
// Build a list of claims matching the parameters extracted from the request.
//
// Note: in most cases, parameters should be representated as strings as requests are
// typically resolved from the query string or the request form, where parameters
// are natively represented as strings. However, requests can also be extracted from
// different places where they can be represented as complex JSON representations
// (e.g requests extracted from a JSON Web Token that may be encrypted and/or signed).
var claims = from parameter in context.Request.GetParameters()
let element = (JsonElement) parameter.Value
let type = element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.Number => ClaimValueTypes.Integer64,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
}
select new Claim(parameter.Key, element.ToString()!, type);
// Store the serialized authorization request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
await _cache.SetStringAsync(Cache.AuthorizationRequest + context.Request.RequestId,
token, _options.CurrentValue.AuthorizationRequestCachingPolicy);
// Create a new GET authorization request containing only the request_id parameter.
var location = WebUtilities.AddQueryString(
uri: new UriBuilder(context.RequestUri) { Query = null }.Uri.AbsoluteUri,
name: Parameters.RequestId,
value: context.Request.RequestId);
request.Context.Response.Redirect(location);
// Mark the response as handled to skip the rest of the pipeline.
context.HandleRequest();
}
public ValueTask HandleAsync(ExtractAuthorizationRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for removing cached authorization requests from the distributed cache.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler<ApplyAuthorizationResponseContext>
{
private readonly IDistributedCache _cache;
public RemoveCachedRequest() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RemoveCachedRequest() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RemoveCachedRequest(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -284,6 +146,28 @@ public static partial class OpenIddictServerOwinHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for processing authorization responses requiring a self-redirection.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public sealed class ProcessSelfRedirection : IOpenIddictServerHandler<ApplyAuthorizationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyAuthorizationResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessSelfRedirection>()
.SetOrder(250_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
{
@ -292,18 +176,37 @@ public static partial class OpenIddictServerOwinHandlers
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request?.RequestId))
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
if (string.IsNullOrEmpty(context.Response.RequestUri))
{
return default;
}
// Note: the ApplyAuthorizationResponse event is called for both successful
// and errored authorization responses but discrimination is not necessary here,
// as the authorization request must be removed from the distributed cache in both cases.
// 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 response = context.Transaction.GetOwinRequest()?.Context.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
return new(_cache.RemoveAsync(Cache.AuthorizationRequest + context.Request.RequestId));
var location = context.RequestUri.GetLeftPart(UriPartial.Path);
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{
location = WebUtilities.AddQueryString(location, key, value);
}
response.Redirect(location);
context.HandleRequest();
return default;
}
}
@ -325,7 +228,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyAuthorizationResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessFormPostResponse>()
.SetOrder(250_000)
.SetOrder(ProcessSelfRedirection.Descriptor.Order)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

242
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs

@ -5,18 +5,10 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using Owin;
using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants;
using JsonWebTokenTypes = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.JsonWebTokenTypes;
namespace OpenIddict.Server.Owin;
@ -29,8 +21,6 @@ public static partial class OpenIddictServerOwinHandlers
* End-session request extraction:
*/
ExtractGetOrPostRequest<ExtractEndSessionRequestContext>.Descriptor,
RestoreCachedRequestParameters.Descriptor,
CacheRequestParameters.Descriptor,
/*
* End-session request handling:
@ -40,15 +30,15 @@ public static partial class OpenIddictServerOwinHandlers
/*
* End-session response processing:
*/
RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyEndSessionResponseContext>.Descriptor,
AttachOwinResponseChallenge<ApplyEndSessionResponseContext>.Descriptor,
SuppressFormsAuthenticationRedirect<ApplyEndSessionResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyEndSessionResponseContext>.Descriptor,
ProcessSelfRedirection.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse.Descriptor,
ProcessPassthroughErrorResponse<ApplyEndSessionResponseContext, RequireEndSessionEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyEndSessionResponseContext>.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessEmptyResponse<ApplyEndSessionResponseContext>.Descriptor
]);
@ -56,14 +46,13 @@ public static partial class OpenIddictServerOwinHandlers
/// Contains the logic responsible for restoring cached requests from the request_id, if specified.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler<ExtractEndSessionRequestContext>
{
private readonly IDistributedCache _cache;
public RestoreCachedRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RestoreCachedRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RestoreCachedRequestParameters(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -78,92 +67,23 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractEndSessionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// If a request_id parameter can be found in the end session request,
// restore the complete end session request from the distributed cache.
if (string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
var token = await _cache.GetStringAsync(Cache.EndSessionRequest + context.Request.RequestId);
if (token is null || !context.Options.JsonWebTokenHandler.CanReadToken(token))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6150), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = [JsonWebTokenTypes.Private.EndSessionRequest];
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters);
if (!result.IsValid)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6150), Parameters.RequestId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.RequestId),
uri: SR.FormatID8000(SR.ID2052));
return;
}
using var document = JsonDocument.Parse(
Base64UrlEncoder.Decode(((JsonWebToken) result.SecurityToken).InnerToken.EncodedPayload));
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0118));
}
// Restore the request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
if (!context.Request.HasParameter(parameter.Name))
{
context.Request.AddParameter(parameter.Name, parameter.Value.Clone());
}
}
}
public ValueTask HandleAsync(ExtractEndSessionRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for caching end session requests, if applicable.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler<ExtractEndSessionRequestContext>
{
private readonly IDistributedCache _cache;
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;
public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor<OpenIddictServerOwinOptions> options)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -178,97 +98,21 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ExtractEndSessionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
// 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() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Don't cache the request if the request doesn't include any parameter.
// If a request_id parameter can be found in the end session request,
// ignore the following logic to prevent an infinite redirect loop.
if (context.Request.Count is 0 || !string.IsNullOrEmpty(context.Request.RequestId))
{
return;
}
// Generate a 256-bit request identifier using a crypto-secure random number generator.
context.Request.RequestId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
// Build a list of claims matching the parameters extracted from the request.
//
// Note: in most cases, parameters should be representated as strings as requests are
// typically resolved from the query string or the request form, where parameters
// are natively represented as strings. However, requests can also be extracted from
// different places where they can be represented as complex JSON representations
// (e.g requests extracted from a JSON Web Token that may be encrypted and/or signed).
var claims = from parameter in context.Request.GetParameters()
let element = (JsonElement) parameter.Value
let type = element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.Number => ClaimValueTypes.Integer64,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
}
select new Claim(parameter.Key, element.ToString()!, type);
// Store the serialized end session request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.EndSessionRequest
});
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
await _cache.SetStringAsync(Cache.EndSessionRequest + context.Request.RequestId,
token, _options.CurrentValue.EndSessionRequestCachingPolicy);
// Create a new GET end session request containing only the request_id parameter.
var location = WebUtilities.AddQueryString(
uri: new UriBuilder(context.RequestUri) { Query = null }.Uri.AbsoluteUri,
name: Parameters.RequestId,
value: context.Request.RequestId);
request.Context.Response.Redirect(location);
// Mark the response as handled to skip the rest of the pipeline.
context.HandleRequest();
}
public ValueTask HandleAsync(ExtractEndSessionRequestContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for removing cached end session requests from the distributed cache.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
[Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler<ApplyEndSessionResponseContext>
{
private readonly IDistributedCache _cache;
public RemoveCachedRequest() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
public RemoveCachedRequest() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public RemoveCachedRequest(IDistributedCache cache)
=> _cache = cache ?? throw new ArgumentNullException(nameof(cache));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -282,6 +126,28 @@ public static partial class OpenIddictServerOwinHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyEndSessionResponseContext context)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Contains the logic responsible for processing end session responses requiring a self-redirection.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public sealed class ProcessSelfRedirection : IOpenIddictServerHandler<ApplyEndSessionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyEndSessionResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessSelfRedirection>()
.SetOrder(250_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyEndSessionResponseContext context)
{
@ -290,18 +156,36 @@ public static partial class OpenIddictServerOwinHandlers
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request?.RequestId))
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
if (string.IsNullOrEmpty(context.Response.RequestUri))
{
return default;
}
// Note: the ApplyEndSessionResponse event is called for both successful
// and errored end session responses but discrimination is not necessary here,
// as the end session request must be removed from the distributed cache in both cases.
// 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 response = context.Transaction.GetOwinRequest()?.Context.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
var location = context.RequestUri.GetLeftPart(UriPartial.Path);
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{
location = WebUtilities.AddQueryString(location, key, value);
}
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached payloads.
return new(_cache.RemoveAsync(Cache.EndSessionRequest + context.Request.RequestId));
response.Redirect(location);
context.HandleRequest();
return default;
}
}
@ -318,7 +202,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyEndSessionResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessQueryResponse>()
.SetOrder(250_000)
.SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

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

@ -936,8 +936,27 @@ public static partial class OpenIddictServerOwinHandlers
throw new ArgumentNullException(nameof(context));
}
context.SkipRequest();
switch (context.EndpointType)
{
// When authorization request caching is enabled and the request doesn't contain a
// request_uri yet, do not enable the pass-through mode to allow OpenIddict to trigger
// a sign-in operation that will generate and attach a request token to the parameters.
case OpenIddictServerEndpointType.Authorization when
context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Transaction.Request?.RequestUri):
return default;
// When end session request caching is enabled and the request doesn't contain a
// request_uri yet, do not enable the pass-through mode to allow OpenIddict to trigger
// a sign-in operation that will generate and attach a request token to the parameters.
case OpenIddictServerEndpointType.EndSession when
context.Options.EnableEndSessionRequestCaching &&
string.IsNullOrEmpty(context.Transaction.Request?.RequestUri):
return default;
}
context.SkipRequest();
return default;
}
}
@ -976,6 +995,10 @@ public static partial class OpenIddictServerOwinHandlers
response.StatusCode = (context.EndpointType, context.Transaction.Response.Error) switch
{
// Note: for pushed authorization responses, the returned HTTP status code MUST be 201.
// See https://datatracker.ietf.org/doc/html/rfc9126#section-2.2 for more information.
(OpenIddictServerEndpointType.PushedAuthorization, null or { Length: 0 }) => 201,
// Note: the default code may be replaced by another handler (e.g when doing redirects).
(_, null or { Length: 0 }) => 200,

4
src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs

@ -82,12 +82,14 @@ public sealed class OpenIddictServerOwinOptions : AuthenticationOptions
/// Enabling this option is recommended when using external authentication providers
/// or when large GET or POST OpenID Connect authorization requests support is required.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableAuthorizationRequestCaching { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether requests received by the end session endpoint should be cached.
/// When enabled, authorization requests are automatically stored in the distributed cache.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableEndSessionRequestCaching { get; set; }
/// <summary>
@ -103,6 +105,7 @@ public sealed class OpenIddictServerOwinOptions : AuthenticationOptions
/// <summary>
/// Gets or sets the caching policy used by the authorization endpoint.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public DistributedCacheEntryOptions AuthorizationRequestCachingPolicy { get; set; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
@ -112,6 +115,7 @@ public sealed class OpenIddictServerOwinOptions : AuthenticationOptions
/// <summary>
/// Gets or sets the caching policy used by the end session endpoint.
/// </summary>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public DistributedCacheEntryOptions EndSessionRequestCachingPolicy { get; set; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),

79
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1385,6 +1385,55 @@ public sealed class OpenIddictServerBuilder
});
}
/// <summary>
/// Sets the relative or absolute URIs associated to the pushed authorization endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first URI will be returned as part of the discovery document.
/// </summary>
/// <param name="uris">The URIs associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetPushedAuthorizationEndpointUris(
[StringSyntax(StringSyntaxAttribute.Uri)] params string[] uris)
{
if (uris is null)
{
throw new ArgumentNullException(nameof(uris));
}
return SetPushedAuthorizationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray());
}
/// <summary>
/// Sets the relative or absolute URIs associated to the pushed authorization endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first URI will be returned as part of the discovery document.
/// </summary>
/// <param name="uris">The URIs associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetPushedAuthorizationEndpointUris(params Uri[] uris)
{
if (uris is null)
{
throw new ArgumentNullException(nameof(uris));
}
if (Array.Exists(uris, OpenIddictHelpers.IsImplicitFileUri))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uris));
}
if (Array.Exists(uris, static uri => uri.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(SR.FormatID0081("~"), nameof(uris));
}
return Configure(options =>
{
options.PushedAuthorizationEndpointUris.Clear();
options.PushedAuthorizationEndpointUris.AddRange(uris);
});
}
/// <summary>
/// Sets the relative or absolute URIs associated to the revocation endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
@ -1745,12 +1794,21 @@ public sealed class OpenIddictServerBuilder
/// <summary>
/// Configures OpenIddict to force client applications to use Proof Key for Code Exchange
/// (PKCE) when requesting an authorization code (e.g when using the code or hybrid flows).
/// When enforced, authorization requests that lack the code_challenge will be rejected.
/// When enforced, authorization requests that lack the code_challenge parameter will be rejected.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder RequireProofKeyForCodeExchange()
=> Configure(options => options.RequireProofKeyForCodeExchange = true);
/// <summary>
/// Configures OpenIddict to force client applications to use pushed authorization requests
/// when using an interactive flow like the authorization code or implicit flows.
/// When enforced, authorization requests that lack the request_id parameter will be rejected.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder RequirePushedAuthorizationRequests()
=> Configure(options => options.RequirePushedAuthorizationRequests = true);
/// <summary>
/// Sets the access token lifetime, after which client applications must retrieve
/// a new access token by making a grant_type=refresh_token token request
@ -1968,6 +2026,25 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder UseReferenceRefreshTokens()
=> Configure(options => options.UseReferenceRefreshTokens = true);
/// <summary>
/// Enables authorization request storage, so that authorization requests
/// are automatically stored in the token store, which allows flowing
/// large payloads across requests. Enabling this option can be useful
/// for clients that do not supported pushed authorization requests.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder EnableAuthorizationRequestCaching()
=> Configure(options => options.EnableAuthorizationRequestCaching = true);
/// <summary>
/// Enables end session request storage, so that end session requests
/// are automatically stored in the token store, which allows flowing
/// large payloads across requests.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder EnableEndSessionRequestCaching()
=> Configure(options => options.EnableEndSessionRequestCaching = true);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);

27
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -7,7 +7,6 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -47,6 +46,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.EnableDegradedMode)
{
options.DisableAuthorizationStorage = options.DisableTokenStorage = options.DisableRollingRefreshTokens = true;
options.EnableAuthorizationRequestCaching = options.EnableEndSessionRequestCaching = false;
options.IgnoreEndpointPermissions = options.IgnoreGrantTypePermissions = true;
options.IgnoreResponseTypePermissions = options.IgnoreScopePermissions = true;
options.UseReferenceAccessTokens = options.UseReferenceRefreshTokens = false;
@ -75,6 +75,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
.Concat(options.DeviceAuthorizationEndpointUris.Distinct())
.Concat(options.IntrospectionEndpointUris.Distinct())
.Concat(options.EndSessionEndpointUris.Distinct())
.Concat(options.PushedAuthorizationEndpointUris.Distinct())
.Concat(options.RevocationEndpointUris.Distinct())
.Concat(options.TokenEndpointUris.Distinct())
.Concat(options.UserInfoEndpointUris.Distinct())
@ -145,10 +146,11 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
}
// Ensure at least one client authentication method is enabled (unless no non-interactive endpoint was enabled).
if (options.ClientAuthenticationMethods.Count is 0 && (options.DeviceAuthorizationEndpointUris.Count is not 0 ||
options.IntrospectionEndpointUris.Count is not 0 ||
options.RevocationEndpointUris.Count is not 0 ||
options.TokenEndpointUris.Count is not 0))
if (options.ClientAuthenticationMethods.Count is 0 && (options.DeviceAuthorizationEndpointUris.Count is not 0 ||
options.IntrospectionEndpointUris.Count is not 0 ||
options.PushedAuthorizationEndpointUris.Count is not 0 ||
options.RevocationEndpointUris.Count is not 0 ||
options.TokenEndpointUris.Count is not 0))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0419));
}
@ -180,6 +182,12 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0083));
}
// Ensure authorization or end session request caching was not enabled when token storage is disabled.
if (options.DisableTokenStorage && (options.EnableAuthorizationRequestCaching || options.EnableEndSessionRequestCaching))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0465));
}
// Prevent the device authorization flow from being used if token storage is disabled, unless the degraded
// mode has been enabled (in this case, additional checks will be enforced later to require custom handlers).
if (options.DisableTokenStorage && !options.EnableDegradedMode && options.GrantTypes.Contains(GrantTypes.DeviceCode))
@ -258,6 +266,15 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0092));
}
if (options.PushedAuthorizationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidatePushedAuthorizationRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0467));
}
if (options.RevocationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidateRevocationRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&

7
src/OpenIddict.Server/OpenIddictServerEndpointType.cs

@ -64,5 +64,10 @@ public enum OpenIddictServerEndpointType
/// <summary>
/// User verification endpoint.
/// </summary>
EndUserVerification = 10
EndUserVerification = 10,
/// <summary>
/// Pushed authorization endpoint.
/// </summary>
PushedAuthorization = 11
}

204
src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs

@ -69,7 +69,7 @@ public static partial class OpenIddictServerEvents
/// the user code by calling <see cref="SetRedirectUri(string)"/>.
/// </summary>
[StringSyntax(StringSyntaxAttribute.Uri)]
public string? RedirectUri { get; private set; }
public string? RedirectUri { get; internal set; }
/// <summary>
/// Gets or sets the security principal extracted
@ -77,6 +77,12 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the
/// request token, if applicable.
/// </summary>
public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
/// <summary>
/// Populates the <see cref="RedirectUri"/> property with the specified redirect_uri.
/// </summary>
@ -219,4 +225,200 @@ public static partial class OpenIddictServerEvents
/// </summary>
public string? ResponseMode { get; set; }
}
/// <summary>
/// Represents an event called for each request to the pushed authorization endpoint to give the
/// user code a chance to manually extract the authorization request from the ambient HTTP context.
/// </summary>
public sealed class ExtractPushedAuthorizationRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractPushedAuthorizationRequestContext"/> class.
/// </summary>
public ExtractPushedAuthorizationRequestContext(OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request or <see langword="null"/> if it was extracted yet.
/// </summary>
public OpenIddictRequest? Request
{
get => Transaction.Request;
set => Transaction.Request = value;
}
}
/// <summary>
/// Represents an event called for each request to the pushed authorization request
/// endpoint to determine if the request is valid and should continue to be processed.
/// </summary>
public sealed class ValidatePushedAuthorizationRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ValidatePushedAuthorizationRequestContext"/> class.
/// </summary>
public ValidatePushedAuthorizationRequestContext(OpenIddictServerTransaction transaction)
: base(transaction)
// Infer the redirect_uri from the value specified by the client application.
=> RedirectUri = Request?.RedirectUri;
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets the client_id specified by the client application.
/// </summary>
public string? ClientId => Request?.ClientId;
/// <summary>
/// Gets the redirect_uri specified by the client application.
/// If it's not provided by the client, it must be set by
/// the user code by calling <see cref="SetRedirectUri(string)"/>.
/// </summary>
[StringSyntax(StringSyntaxAttribute.Uri)]
public string? RedirectUri { get; private set; }
/// <summary>
/// Gets or sets the security principal extracted
/// from the identity token hint, if applicable.
/// </summary>
public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
/// <summary>
/// Populates the <see cref="RedirectUri"/> property with the specified redirect_uri.
/// </summary>
/// <param name="uri">The redirect_uri to use when redirecting the user agent.</param>
public void SetRedirectUri([StringSyntax(StringSyntaxAttribute.Uri)] string uri)
{
if (string.IsNullOrEmpty(uri))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0100), nameof(uri));
}
// Don't allow validation to alter the redirect_uri parameter extracted
// from the request if the URI was explicitly provided by the client.
if (!string.IsNullOrEmpty(Request?.RedirectUri) &&
!string.Equals(Request.RedirectUri, uri, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0101));
}
RedirectUri = uri;
}
}
/// <summary>
/// Represents an event called for each validated pushed authorization request
/// to allow the user code to decide how the request should be handled.
/// </summary>
public sealed class HandlePushedAuthorizationRequestContext : BaseValidatingTicketContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandlePushedAuthorizationRequestContext"/> class.
/// </summary>
public HandlePushedAuthorizationRequestContext(OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the security principal extracted
/// from the identity token hint, if applicable.
/// </summary>
public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
/// <summary>
/// Gets the additional parameters returned to the client application.
/// </summary>
public Dictionary<string, OpenIddictParameter> Parameters { get; private set; }
= new(StringComparer.Ordinal);
/// <summary>
/// Allows OpenIddict to return a sign-in response using the specified principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
public void SignIn(ClaimsPrincipal principal) => Principal = principal;
/// <summary>
/// Allows OpenIddict to return a sign-in response using the specified principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <param name="parameters">The additional parameters returned to the client application.</param>
public void SignIn(ClaimsPrincipal principal, IDictionary<string, OpenIddictParameter> parameters)
{
Principal = principal;
Parameters = new(parameters, StringComparer.Ordinal);
}
}
/// <summary>
/// Represents an event called before the pushed authorization response is returned to the caller.
/// </summary>
public sealed class ApplyPushedAuthorizationResponseContext : BaseRequestContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyAuthorizationResponseContext"/> class.
/// </summary>
public ApplyPushedAuthorizationResponseContext(OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request, or <see langword="null"/> if it couldn't be extracted.
/// </summary>
public OpenIddictRequest? Request
{
get => Transaction.Request;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response.
/// </summary>
public OpenIddictResponse Response
{
get => Transaction.Response!;
set => Transaction.Response = value;
}
/// <summary>
/// Gets the access code expected to
/// be returned to the client application.
/// Depending on the flow, it may be null.
/// </summary>
public string? AccessToken => Response?.AccessToken;
/// <summary>
/// Gets the authorization code expected to
/// be returned to the client application.
/// Depending on the flow, it may be null.
/// </summary>
public string? AuthorizationCode => Response?.Code;
/// <summary>
/// Gets the error code returned to the client application.
/// When the response indicates a successful response,
/// this property returns <see langword="null"/>.
/// </summary>
public string? Error => Response?.Error;
}
}

16
src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs

@ -116,6 +116,11 @@ public static partial class OpenIddictServerEvents
/// </summary>
public Uri? IntrospectionEndpoint { get; set; }
/// <summary>
/// Gets or sets the pushed authorization endpoint URI.
/// </summary>
public Uri? PushedAuthorizationEndpoint { get; set; }
/// <summary>
/// Gets or sets the revocation endpoint URI.
/// </summary>
@ -171,6 +176,12 @@ public static partial class OpenIddictServerEvents
/// </summary>
public HashSet<string> PromptValues { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets a list of client authentication methods supported by the pushed
/// authorization endpoint provided by the authorization server.
/// </summary>
public HashSet<string> PushedAuthorizationEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the list of response modes
/// supported by the authorization server.
@ -206,6 +217,11 @@ public static partial class OpenIddictServerEvents
/// the token endpoint provided by the authorization server.
/// </summary>
public HashSet<string> TokenEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets a boolean indicating whether pushed authorization requests are required.
/// </summary>
public bool RequirePushedAuthorizationRequests { get; set; }
}
/// <summary>

8
src/OpenIddict.Server/OpenIddictServerEvents.Session.cs

@ -67,7 +67,7 @@ public static partial class OpenIddictServerEvents
/// Gets the post_logout_redirect_uri specified by the client application.
/// </summary>
[StringSyntax(StringSyntaxAttribute.Uri)]
public string? PostLogoutRedirectUri { get; private set; }
public string? PostLogoutRedirectUri { get; internal set; }
/// <summary>
/// Gets or sets the security principal extracted
@ -75,6 +75,12 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the
/// request token, if applicable.
/// </summary>
public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
/// <summary>
/// Populates the <see cref="PostLogoutRedirectUri"/> property with the specified redirect_uri.
/// </summary>

77
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -381,6 +381,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ExtractRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a request token
/// should be extracted from the current context.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ExtractRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user
/// code should be extracted from the current context.
@ -453,6 +462,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RequireRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a request token
/// must be resolved for the authentication to be considered valid.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RequireRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user code
/// must be resolved for the authentication to be considered valid.
@ -525,6 +543,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ValidateRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the request token
/// extracted from the current request should be validated.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ValidateRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the user
/// code extracted from the current request should be validated.
@ -597,6 +624,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RejectRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid request token
/// will cause the authentication demand to be rejected or will be ignored.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid user code
/// will cause the authentication demand to be rejected or will be ignored.
@ -686,6 +722,16 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the request token to validate, if applicable.
/// </summary>
public string? RequestToken { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the request token, if applicable.
/// </summary>
public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the user code to validate, if applicable.
/// </summary>
@ -825,6 +871,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool GenerateRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a request token
/// should be generated (and optionally returned to the client).
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool GenerateRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user code
/// should be generated (and optionally returned to the client).
@ -879,6 +934,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool IncludeRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated request token
/// should be returned to the client application as part of the response.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool IncludeRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated user code
/// should be returned to the client application as part of the response.
@ -953,6 +1017,19 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the generated request token, if applicable.
/// The request token will only be returned if
/// <see cref="IncludeRequestToken"/> is set to <see langword="true"/>.
/// </summary>
public string? RequestToken { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that will be used
/// to create the request token, if applicable.
/// </summary>
public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the generated user code, if applicable.
/// The user code will only be returned if

4
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -67,10 +67,14 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireJsonWebKeySetRequest>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectUriParameter>();
builder.Services.TryAddSingleton<RequirePushedAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireReferenceAccessTokensEnabled>();
builder.Services.TryAddSingleton<RequireReferenceRefreshTokensEnabled>();
builder.Services.TryAddSingleton<RequireRefreshTokenGenerated>();
builder.Services.TryAddSingleton<RequireRefreshTokenValidated>();
builder.Services.TryAddSingleton<RequireRequestTokenGenerated>();
builder.Services.TryAddSingleton<RequireRequestTokenPrincipal>();
builder.Services.TryAddSingleton<RequireRequestTokenValidated>();
builder.Services.TryAddSingleton<RequireResponseTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireRevocationRequest>();
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();

68
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -470,6 +470,23 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a pushed authorization request.
/// </summary>
public sealed class RequirePushedAuthorizationRequest : IOpenIddictServerHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.EndpointType is OpenIddictServerEndpointType.PushedAuthorization);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference access tokens are disabled.
/// </summary>
@ -504,6 +521,57 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no request token is generated.
/// </summary>
public sealed class RequireRequestTokenGenerated : IOpenIddictServerHandlerFilter<ProcessSignInContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateRequestToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no request token principal is available.
/// </summary>
public sealed class RequireRequestTokenPrincipal : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.RequestTokenPrincipal is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no request token is validated.
/// </summary>
public sealed class RequireRequestTokenValidated : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ValidateRequestToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no refresh token is generated.
/// </summary>

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

File diff suppressed because it is too large

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

@ -43,6 +43,7 @@ public static partial class OpenIddictServerHandlers
AttachSubjectTypes.Descriptor,
AttachPromptValues.Descriptor,
AttachSigningAlgorithms.Descriptor,
AttachSecurityRequirements.Descriptor,
AttachAdditionalMetadata.Descriptor,
/*
@ -242,6 +243,7 @@ public static partial class OpenIddictServerHandlers
[Metadata.RevocationEndpoint] = notification.RevocationEndpoint?.AbsoluteUri,
[Metadata.UserInfoEndpoint] = notification.UserInfoEndpoint?.AbsoluteUri,
[Metadata.DeviceAuthorizationEndpoint] = notification.DeviceAuthorizationEndpoint?.AbsoluteUri,
[Metadata.PushedAuthorizationRequestEndpoint] = notification.PushedAuthorizationEndpoint?.AbsoluteUri,
[Metadata.JwksUri] = notification.JsonWebKeySetEndpoint?.AbsoluteUri,
[Metadata.GrantTypesSupported] = notification.GrantTypes.ToArray(),
[Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToArray(),
@ -255,7 +257,9 @@ public static partial class OpenIddictServerHandlers
[Metadata.TokenEndpointAuthMethodsSupported] = notification.TokenEndpointAuthenticationMethods.ToArray(),
[Metadata.IntrospectionEndpointAuthMethodsSupported] = notification.IntrospectionEndpointAuthenticationMethods.ToArray(),
[Metadata.RevocationEndpointAuthMethodsSupported] = notification.RevocationEndpointAuthenticationMethods.ToArray(),
[Metadata.DeviceAuthorizationEndpointAuthMethodsSupported] = notification.DeviceAuthorizationEndpointAuthenticationMethods.ToArray()
[Metadata.DeviceAuthorizationEndpointAuthMethodsSupported] = notification.DeviceAuthorizationEndpointAuthenticationMethods.ToArray(),
[Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported] = notification.PushedAuthorizationEndpointAuthenticationMethods.ToArray(),
[Metadata.RequirePushedAuthorizationRequests] = notification.RequirePushedAuthorizationRequests
};
foreach (var metadata in notification.Metadata)
@ -373,17 +377,20 @@ public static partial class OpenIddictServerHandlers
context.AuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.AuthorizationEndpointUris.FirstOrDefault());
context.JsonWebKeySetEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.JsonWebKeySetEndpointUris.FirstOrDefault());
context.DeviceAuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.DeviceAuthorizationEndpointUris.FirstOrDefault());
context.EndSessionEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.EndSessionEndpointUris.FirstOrDefault());
context.IntrospectionEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.IntrospectionEndpointUris.FirstOrDefault());
context.EndSessionEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.EndSessionEndpointUris.FirstOrDefault());
context.JsonWebKeySetEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.JsonWebKeySetEndpointUris.FirstOrDefault());
context.PushedAuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.PushedAuthorizationEndpointUris.FirstOrDefault());
context.RevocationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.RevocationEndpointUris.FirstOrDefault());
@ -540,6 +547,13 @@ public static partial class OpenIddictServerHandlers
context.IntrospectionEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
}
// Note: "pushed_authorization_request_endpoint_auth_methods_supported" is not a standard parameter
// but is supported by OpenIddict 6.1.0 and higher for consistency with the other endpoints.
if (context.PushedAuthorizationEndpoint is not null)
{
context.PushedAuthorizationEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
}
if (context.RevocationEndpoint is not null)
{
context.RevocationEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
@ -770,6 +784,35 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching the security requirements to the provider discovery document.
/// </summary>
public sealed class AttachSecurityRequirements : IOpenIddictServerHandler<HandleConfigurationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachSecurityRequirements>()
.SetOrder(AttachSigningAlgorithms.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.RequirePushedAuthorizationRequests = context.Options.RequirePushedAuthorizationRequests;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching additional metadata to the provider discovery document.
/// </summary>
@ -781,7 +824,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachAdditionalMetadata>()
.SetOrder(AttachSigningAlgorithms.Descriptor.Order + 1_000)
.SetOrder(AttachSecurityRequirements.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

13
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -187,7 +187,7 @@ public static partial class OpenIddictServerHandlers
0 => null,
// Otherwise, map the token types to their JWT public or internal representation.
_ => context.ValidTokenTypes.SelectMany<string, string>(type =>type switch
_ => context.ValidTokenTypes.SelectMany<string, string>(type => type switch
{
// For access tokens, both "at+jwt" and "application/at+jwt" are valid.
TokenTypeHints.AccessToken =>
@ -215,6 +215,10 @@ public static partial class OpenIddictServerHandlers
// For user codes, only the short "oi_usrc+jwt" form is valid.
TokenTypeHints.UserCode => [JsonWebTokenTypes.Private.UserCode],
// For user codes, only the short "oi_pshaurt+jwt" form is valid.
TokenTypeHints.Private.RequestToken
=> [JsonWebTokenTypes.Private.RequestToken],
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
})
};
@ -543,6 +547,8 @@ public static partial class OpenIddictServerHandlers
JsonWebTokenTypes.Private.RefreshToken => TokenTypeHints.RefreshToken,
JsonWebTokenTypes.Private.UserCode => TokenTypeHints.UserCode,
JsonWebTokenTypes.Private.RequestToken => TokenTypeHints.Private.RequestToken,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
});
@ -1449,7 +1455,8 @@ public static partial class OpenIddictServerHandlers
// For authorization/device/user codes and refresh tokens,
// attach claims destinations to the JWT claims collection.
if (context.TokenType is TokenTypeHints.AuthorizationCode or TokenTypeHints.DeviceCode or
TokenTypeHints.RefreshToken or TokenTypeHints.UserCode)
TokenTypeHints.RefreshToken or TokenTypeHints.UserCode or
TokenTypeHints.Private.RequestToken)
{
var destinations = principal.GetDestinations();
if (destinations.Count is not 0)
@ -1478,6 +1485,8 @@ public static partial class OpenIddictServerHandlers
TokenTypeHints.RefreshToken => JsonWebTokenTypes.Private.RefreshToken,
TokenTypeHints.UserCode => JsonWebTokenTypes.Private.UserCode,
TokenTypeHints.Private.RequestToken => JsonWebTokenTypes.Private.RequestToken,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
}
};

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -28,13 +29,16 @@ public static partial class OpenIddictServerHandlers
HandleEndSessionRequest.Descriptor,
ApplyEndSessionResponse<ProcessErrorContext>.Descriptor,
ApplyEndSessionResponse<ProcessRequestContext>.Descriptor,
ApplyEndSessionResponse<ProcessSignInContext>.Descriptor,
ApplyEndSessionResponse<ProcessSignOutContext>.Descriptor,
/*
* End-session request validation:
*/
ValidateRequestUriParameter.Descriptor,
ValidatePostLogoutRedirectUriParameter.Descriptor,
ValidateAuthentication.Descriptor,
RestorePushedAuthorizationRequestParameters.Descriptor,
ValidateClientPostLogoutRedirectUri.Descriptor,
ValidateEndpointPermissions.Descriptor,
ValidateAuthorizedParty.Descriptor,
@ -266,6 +270,47 @@ public static partial class OpenIddictServerHandlers
}
}
else if (context.Options.EnableEndSessionRequestCaching &&
string.IsNullOrEmpty(context.Transaction.Request?.RequestUri))
{
var @event = new ProcessSignInContext(context.Transaction)
{
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Response = new OpenIddictResponse()
};
if (notification.Parameters.Count > 0)
{
foreach (var parameter in notification.Parameters)
{
@event.Parameters.Add(parameter.Key, parameter.Value);
}
}
await _dispatcher.DispatchAsync(@event);
if (@event.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (@event.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (@event.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0051));
}
}
@ -318,6 +363,63 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting authorization requests that specify an invalid request_uri parameter.
/// </summary>
public sealed class ValidateRequestUriParameter : IOpenIddictServerHandler<ValidateEndSessionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateEndSessionRequestContext>()
.UseSingletonHandler<ValidateRequestUriParameter>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateEndSessionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request.RequestUri))
{
return default;
}
// OpenIddict only supports "request_uri" parameters containing a reference to a request token
// generated via the automatic request caching feature. Since OpenIddict uses a specific URN
// prefix for request tokens it generates, all the other values are automatically rejected.
if (!context.Request.RequestUri.StartsWith(RequestUris.Prefixes.Generic, StringComparison.Ordinal))
{
context.Reject(
error: Errors.RequestUriNotSupported,
description: SR.FormatID2028(Parameters.RequestUri),
uri: SR.FormatID8000(SR.ID2028));
return default;
}
// For consistency with authorization requests, the client_id parameter
// is also required when using a request_uri parameter is present.
if (string.IsNullOrEmpty(context.Request.ClientId))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.RequestUri, Parameters.ClientId),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting end session requests that specify an invalid post_logout_redirect_uri parameter.
/// </summary>
@ -431,8 +533,51 @@ public static partial class OpenIddictServerHandlers
return;
}
// Attach the security principal extracted from the token to the validation context.
// Attach the security principals extracted from the tokens to the validation context.
context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal;
context.RequestTokenPrincipal = notification.RequestTokenPrincipal;
}
}
/// <summary>
/// Contains the logic responsible for restoring the parameters attached to the pushed authorization request.
/// </summary>
public sealed class RestorePushedAuthorizationRequestParameters : IOpenIddictServerHandler<ValidateEndSessionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateEndSessionRequestContext>()
.UseSingletonHandler<RestorePushedAuthorizationRequestParameters>()
.SetOrder(ValidateAuthentication.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateEndSessionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var value = context.RequestTokenPrincipal?.GetClaim(Claims.Private.RequestParameters);
if (string.IsNullOrEmpty(value))
{
return default;
}
using var document = JsonDocument.Parse(value);
var request = new OpenIddictRequest(document.RootElement.Clone())
{
RequestUri = context.Request.RequestUri
};
context.Request = request;
context.PostLogoutRedirectUri = request.PostLogoutRedirectUri;
return default;
}
}
@ -458,7 +603,7 @@ public static partial class OpenIddictServerHandlers
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequirePostLogoutRedirectUriParameter>()
.UseScopedHandler<ValidateClientPostLogoutRedirectUri>()
.SetOrder(ValidateAuthentication.Descriptor.Order + 1_000)
.SetOrder(RestorePushedAuthorizationRequestParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -838,7 +983,9 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Request is null)
// If the end session response contains a request token, do not use the
// post_logout_redirect_uri, as the user agent will be redirected to the same page.
if (context.Request is null || !string.IsNullOrEmpty(context.Response.RequestUri))
{
return default;
}
@ -880,8 +1027,11 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
// Attach the request state to the end session response.
if (string.IsNullOrEmpty(context.Response.State))
// If the user agent is expected to be redirected to the client application, attach the request
// state to the end session response to help the client mitigate CSRF/session fixation attacks.
//
// Note: don't override the state if one was already attached to the response instance.
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri) && string.IsNullOrEmpty(context.Response.State))
{
context.Response.State = context.Request?.State;
}

663
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -43,6 +43,8 @@ public static partial class OpenIddictServerHandlers
ValidateClientAssertionWellknownClaims.Descriptor,
ValidateClientAssertionIssuer.Descriptor,
ValidateClientAssertionAudience.Descriptor,
ValidateRequestToken.Descriptor,
ValidateRequestTokenType.Descriptor,
ValidateAccessToken.Descriptor,
ValidateAuthorizationCode.Descriptor,
ValidateDeviceCode.Descriptor,
@ -50,6 +52,7 @@ public static partial class OpenIddictServerHandlers
ValidateIdentityToken.Descriptor,
ValidateRefreshToken.Descriptor,
ValidateUserCode.Descriptor,
ResolveHostAuthenticationProperties.Descriptor,
ReformatValidatedTokens.Descriptor,
@ -78,6 +81,7 @@ public static partial class OpenIddictServerHandlers
PrepareAccessTokenPrincipal.Descriptor,
PrepareAuthorizationCodePrincipal.Descriptor,
PrepareDeviceCodePrincipal.Descriptor,
PrepareRequestTokenPrincipal.Descriptor,
PrepareRefreshTokenPrincipal.Descriptor,
PrepareIdentityTokenPrincipal.Descriptor,
PrepareUserCodePrincipal.Descriptor,
@ -85,6 +89,7 @@ public static partial class OpenIddictServerHandlers
GenerateAccessToken.Descriptor,
GenerateAuthorizationCode.Descriptor,
GenerateDeviceCode.Descriptor,
GenerateRequestToken.Descriptor,
GenerateRefreshToken.Descriptor,
AttachDeviceCodeIdentifier.Descriptor,
@ -103,6 +108,7 @@ public static partial class OpenIddictServerHandlers
* Sign-out processing:
*/
ValidateSignOutDemand.Descriptor,
RedeemLogoutTokenEntry.Descriptor,
AttachCustomSignOutParameters.Descriptor,
/*
@ -158,6 +164,7 @@ public static partial class OpenIddictServerHandlers
Matches(context.Options.EndUserVerificationEndpointUris) ? OpenIddictServerEndpointType.EndUserVerification :
Matches(context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection :
Matches(context.Options.JsonWebKeySetEndpointUris) ? OpenIddictServerEndpointType.JsonWebKeySet :
Matches(context.Options.PushedAuthorizationEndpointUris) ? OpenIddictServerEndpointType.PushedAuthorization :
Matches(context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation :
Matches(context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token :
Matches(context.Options.UserInfoEndpointUris) ? OpenIddictServerEndpointType.UserInfo :
@ -242,8 +249,9 @@ public static partial class OpenIddictServerHandlers
{
OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.DeviceAuthorization or
OpenIddictServerEndpointType.EndSession or OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation or
OpenIddictServerEndpointType.Token or OpenIddictServerEndpointType.UserInfo
OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.PushedAuthorization or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token or
OpenIddictServerEndpointType.UserInfo
=> default,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0002)),
@ -305,8 +313,8 @@ public static partial class OpenIddictServerHandlers
// Client assertions can be used with all the endpoints that support client authentication.
// By default, client assertions are not required, but they are extracted and validated if
// present and invalid client assertions are always automatically rejected by OpenIddict.
OpenIddictServerEndpointType.DeviceAuthorization or OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token
OpenIddictServerEndpointType.DeviceAuthorization or OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token
=> (true, false, true, true),
_ => (false, false, false, false)
@ -342,17 +350,31 @@ public static partial class OpenIddictServerHandlers
context.ValidateIdentityToken,
context.RejectIdentityToken) = context.EndpointType switch
{
// The identity token received by the authorization and logout
// endpoints are not required and serve as optional hints.
// The identity token received by the authorization, end session and pushed
// authorization endpoints are not required and serve as optional hints.
//
// As such, identity token hints are extracted and validated, but
// the authentication demand is not rejected if they are not valid.
OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.EndSession
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization
=> (true, false, true, false),
_ => (false, false, false, false)
};
(context.ExtractRequestToken,
context.RequireRequestToken,
context.ValidateRequestToken,
context.RejectRequestToken) = context.EndpointType switch
{
// Always validate request tokens received by the authorization or end session endpoints.
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession => (true, false, true, true),
_ => (false, false, false, false)
};
(context.ExtractRefreshToken,
context.RequireRefreshToken,
context.ValidateRefreshToken,
@ -450,12 +472,30 @@ public static partial class OpenIddictServerHandlers
context.IdentityToken = context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession when context.ExtractIdentityToken
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization when context.ExtractIdentityToken
=> context.Request.IdTokenHint,
_ => null
};
context.RequestToken = context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization when
context.ExtractRequestToken &&
context.Request.RequestUri is { Length: > 0 } uri &&
uri.StartsWith(RequestUris.Prefixes.Generic, StringComparison.OrdinalIgnoreCase)
=> uri[RequestUris.Prefixes.Generic.Length..],
OpenIddictServerEndpointType.EndSession when
context.ExtractRequestToken &&
context.Request.RequestUri is { Length: > 0 } uri &&
uri.StartsWith(RequestUris.Prefixes.Generic, StringComparison.OrdinalIgnoreCase)
=> uri[RequestUris.Prefixes.Generic.Length..],
_ => null
};
context.RefreshToken = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.ExtractRefreshToken
@ -508,6 +548,7 @@ public static partial class OpenIddictServerHandlers
(context.RequireGenericToken && string.IsNullOrEmpty(context.GenericToken)) ||
(context.RequireIdentityToken && string.IsNullOrEmpty(context.IdentityToken)) ||
(context.RequireRefreshToken && string.IsNullOrEmpty(context.RefreshToken)) ||
(context.RequireRequestToken && string.IsNullOrEmpty(context.RequestToken)) ||
(context.RequireUserCode && string.IsNullOrEmpty(context.UserCode)))
{
context.Reject(
@ -900,6 +941,14 @@ public static partial class OpenIddictServerHandlers
return true;
}
// If the current request is a pushed authorization request, consider the audience valid
// if the address matches one of the URIs assigned to the pushed authorization endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.PushedAuthorization &&
MatchesAnyUri(uri, context.Options.PushedAuthorizationEndpointUris))
{
return true;
}
// If the current request is a revocation request, consider the audience valid
// if the address matches one of the URIs assigned to the revocation endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.Revocation &&
@ -1037,8 +1086,8 @@ public static partial class OpenIddictServerHandlers
error: context.EndpointType switch
{
// For non-interactive endpoints, return "invalid_client" instead of "invalid_request".
OpenIddictServerEndpointType.DeviceAuthorization or OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token
OpenIddictServerEndpointType.DeviceAuthorization or OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token
=> Errors.InvalidClient,
_ => Errors.InvalidRequest
@ -1092,6 +1141,7 @@ public static partial class OpenIddictServerHandlers
if (context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.PushedAuthorization or
OpenIddictServerEndpointType.UserInfo)
{
return;
@ -1201,6 +1251,7 @@ public static partial class OpenIddictServerHandlers
if (context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.PushedAuthorization or
OpenIddictServerEndpointType.UserInfo)
{
return;
@ -1229,6 +1280,130 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the request token resolved from the context.
/// </summary>
public sealed class ValidateRequestToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
public ValidateRequestToken(IOpenIddictServerDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRequestTokenValidated>()
.UseScopedHandler<ValidateRequestToken>()
.SetOrder(ValidateClientSecret.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.RequestToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Token = context.RequestToken,
ValidTokenTypes = { TokenTypeHints.Private.RequestToken }
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
if (context.RejectRequestToken)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
return;
}
context.RequestTokenPrincipal = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible for ensuring the resolved request
/// token is suitable for the requested authentication demand.
/// </summary>
public sealed class ValidateRequestTokenType : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRequestTokenPrincipal>()
.AddFilter<RequireRequestTokenValidated>()
.UseSingletonHandler<ValidateRequestTokenType>()
.SetOrder(ValidateRequestToken.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.RequestTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Reject the authentication demand if the request token is not expected to be
// received by the current endpoint as it may indicate a mix-up attack (e.g a request
// token created for an end session request was used for an authorization request).
switch ((context.EndpointType, context.RequestTokenPrincipal.GetClaim(Claims.Private.RequestTokenType)))
{
case (OpenIddictServerEndpointType.Authorization, not (
RequestTokenTypes.Private.CachedAuthorizationRequest or
RequestTokenTypes.Private.PushedAuthorizationRequest)):
case (OpenIddictServerEndpointType.EndSession, not RequestTokenTypes.Private.CachedEndSessionRequest):
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2182(Parameters.RequestUri),
uri: SR.FormatID8000(SR.ID2182));
return default;
// For other endpoints that don't natively support request tokens, don't return an error
// to allow custom implementations to use request tokens with other types of endpoints.
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the access token resolved from the context.
/// </summary>
@ -1246,7 +1421,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAccessTokenValidated>()
.UseScopedHandler<ValidateAccessToken>()
.SetOrder(ValidateClientSecret.Descriptor.Order + 1_000)
.SetOrder(ValidateRequestTokenType.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -1573,7 +1748,8 @@ public static partial class OpenIddictServerHandlers
{
// Don't validate the lifetime of id_tokens used as id_token_hints.
DisableLifetimeValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession,
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization,
Token = context.IdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -1906,6 +2082,7 @@ public static partial class OpenIddictServerHandlers
if (context.EndpointType is not (OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.PushedAuthorization or
OpenIddictServerEndpointType.Token or
OpenIddictServerEndpointType.UserInfo))
{
@ -1941,7 +2118,9 @@ public static partial class OpenIddictServerHandlers
context.Response.Error ??= context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.EndUserVerification
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.PushedAuthorization
=> Errors.AccessDenied,
OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
@ -1952,7 +2131,9 @@ public static partial class OpenIddictServerHandlers
context.Response.ErrorDescription ??= context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.EndUserVerification
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.PushedAuthorization
=> SR.GetResourceString(SR.ID2015),
OpenIddictServerEndpointType.Token => SR.GetResourceString(SR.ID2024),
@ -1963,7 +2144,9 @@ public static partial class OpenIddictServerHandlers
context.Response.ErrorUri ??= context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.EndUserVerification
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.PushedAuthorization
=> SR.FormatID8000(SR.ID2015),
OpenIddictServerEndpointType.Token => SR.FormatID8000(SR.ID2024),
@ -2154,46 +2337,62 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.EndpointType is not (OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.DeviceAuthorization or
OpenIddictServerEndpointType.EndUserVerification or
OpenIddictServerEndpointType.Token))
switch (context.EndpointType)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0010));
}
// Note: sign-in operations triggered from the device authorization or pushed authorization endpoints
// can't be associated to specific users as users' identity is not known until they reach the end-user
// verification endpoint and validate the user code (for the device authorization flow) or are redirected
// to the authorization endpoint and approve the demand (for an interactive flow like the code flow).
//
// As such, the principal used in this case cannot contain an authenticated identity or a subject claim.
case OpenIddictServerEndpointType.DeviceAuthorization:
case OpenIddictServerEndpointType.PushedAuthorization:
// Similarly, sign-in operations triggered from the authorization or end session endpoints
// when the built-in request caching (that stores requests as request tokens in the database)
// is enabled cannot be associated to a specific user or contain an authenticated identity.
case OpenIddictServerEndpointType.Authorization
when context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri):
case OpenIddictServerEndpointType.EndSession
when context.Options.EnableEndSessionRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri):
if (context.Principal is not { Identity: ClaimsIdentity })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0011));
}
if (context.Principal is not { Identity: ClaimsIdentity })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0011));
}
if (context.Principal.Identity.IsAuthenticated)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0012));
}
// Note: sign-in operations triggered from the device authorization endpoint can't be associated to specific users
// as users' identity is not known until they reach the end-user verification endpoint and validate the user code.
// As such, the principal used in this case cannot contain an authenticated identity or a subject claim.
if (context.EndpointType is OpenIddictServerEndpointType.DeviceAuthorization)
{
if (context.Principal.Identity.IsAuthenticated)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0012));
}
if (context.Principal.HasClaim(Claims.Subject))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0013));
}
break;
if (context.Principal.HasClaim(Claims.Subject))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0013));
}
}
case OpenIddictServerEndpointType.Authorization:
case OpenIddictServerEndpointType.EndUserVerification:
case OpenIddictServerEndpointType.Token:
if (context.Principal is not { Identity: ClaimsIdentity })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0011));
}
else
{
if (!context.Principal.Identity.IsAuthenticated)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0014));
}
if (!context.Principal.Identity.IsAuthenticated)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0014));
}
if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0015));
}
if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0015));
}
break;
default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0010));
}
foreach (var group in context.Principal.Claims
@ -2238,7 +2437,8 @@ public static partial class OpenIddictServerHandlers
// The following claims MUST be represented as unique integers.
Claims.Private.AccessTokenLifetime or Claims.Private.AuthorizationCodeLifetime or
Claims.Private.DeviceCodeLifetime or Claims.Private.IdentityTokenLifetime or
Claims.Private.RefreshTokenLifetime or Claims.Private.RefreshTokenLifetime
Claims.Private.RefreshTokenLifetime or Claims.Private.RefreshTokenLifetime or
Claims.Private.RequestTokenLifetime
=> values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
ClaimValueTypes.Integer64 or ClaimValueTypes.UInteger32 or
ClaimValueTypes.UInteger64 }],
@ -2256,8 +2456,7 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to
/// the received authorization code, device code, user code or refresh token.
/// Contains the logic responsible for redeeming the token entry corresponding to the received token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class RedeemTokenEntry : IOpenIddictServerHandler<ProcessSignInContext>
@ -2294,6 +2493,7 @@ public static partial class OpenIddictServerHandlers
switch (context.EndpointType)
{
case OpenIddictServerEndpointType.Authorization:
case OpenIddictServerEndpointType.EndUserVerification:
case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType():
@ -2310,6 +2510,8 @@ public static partial class OpenIddictServerHandlers
var principal = context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization => notification.RequestTokenPrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> notification.AuthorizationCodePrincipal,
@ -2324,7 +2526,10 @@ public static partial class OpenIddictServerHandlers
_ => null
};
Debug.Assert(principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
if (principal is null)
{
return;
}
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
@ -2632,6 +2837,13 @@ public static partial class OpenIddictServerHandlers
(context.GenerateAccessToken, context.IncludeAccessToken) = context.EndpointType switch
{
// Never generate an access token if request caching was enabled and the authorization
// request doesn't already contain a request_uri parameter, as the user agent will be
// redirected to the authorization endpoint after generating a request token.
OpenIddictServerEndpointType.Authorization when
context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri) => (false, false),
// For authorization requests, generate and return an access token
// if a response type containing the "token" value was specified.
OpenIddictServerEndpointType.Authorization when context.Request.HasResponseType(ResponseTypes.Token)
@ -2645,6 +2857,13 @@ public static partial class OpenIddictServerHandlers
(context.GenerateAuthorizationCode, context.IncludeAuthorizationCode) = context.EndpointType switch
{
// Never generate an authorization code if request caching was enabled and the authorization
// request doesn't already contain a request_uri parameter, as the user agent will be
// redirected to the authorization endpoint after generating a request token.
OpenIddictServerEndpointType.Authorization when
context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri) => (false, false),
// For authorization requests, generate and return an authorization code
// if a response type containing the "code" value was specified.
OpenIddictServerEndpointType.Authorization when context.Request.HasResponseType(ResponseTypes.Code)
@ -2669,6 +2888,13 @@ public static partial class OpenIddictServerHandlers
(context.GenerateIdentityToken, context.IncludeIdentityToken) = context.EndpointType switch
{
// Never generate an identity token if request caching was enabled and the authorization
// request doesn't contain a request_uri parameter, as the user agent will be
// redirected to the authorization endpoint after generating a request token.
OpenIddictServerEndpointType.Authorization when
context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri) => (false, false),
// For authorization requests, generate and return an identity token if a response type
// containing code was specified and if the openid scope was explicitly or implicitly granted.
OpenIddictServerEndpointType.Authorization when
@ -2681,6 +2907,26 @@ public static partial class OpenIddictServerHandlers
_ => (false, false)
};
(context.GenerateRequestToken, context.IncludeRequestToken) = context.EndpointType switch
{
// Always generate a request token if request caching was enabled and the
// authorization request doesn't already contain a request_uri parameter.
OpenIddictServerEndpointType.Authorization when
context.Options.EnableAuthorizationRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri) => (true, true),
// Always generate a request token if request caching was enabled and the
// end session request doesn't already contain a request_uri parameter.
OpenIddictServerEndpointType.EndSession when
context.Options.EnableEndSessionRequestCaching &&
string.IsNullOrEmpty(context.Request.RequestUri) => (true, true),
// Always generate and return a request token if the request is a PAR request.
OpenIddictServerEndpointType.PushedAuthorization => (true, true),
_ => (false, false)
};
(context.GenerateRefreshToken, context.IncludeRefreshToken) = context.EndpointType switch
{
// For token requests, allow a refresh token to be returned
@ -3210,6 +3456,138 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal used
/// to generate the request token, if one is going to be returned.
/// </summary>
public sealed class PrepareRequestTokenPrincipal : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictApplicationManager? _applicationManager;
public PrepareRequestTokenPrincipal(IOpenIddictApplicationManager? applicationManager = null)
=> _applicationManager = applicationManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireRequestTokenGenerated>()
.UseScopedHandler<PrepareRequestTokenPrincipal>(static provider =>
{
// Note: the application manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
return options.EnableDegradedMode ?
new PrepareRequestTokenPrincipal() :
new PrepareRequestTokenPrincipal(provider.GetService<IOpenIddictApplicationManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(PrepareDeviceCodePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Create a new principal containing only the filtered claims.
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Other claims are always included in the device code, even private claims.
return true;
});
principal.SetCreationDate(
#if SUPPORTS_TIME_PROVIDER
context.Options.TimeProvider?.GetUtcNow() ??
#endif
DateTimeOffset.UtcNow);
// If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.Principal.GetRequestTokenLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
{
if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));
var settings = await _applicationManager.GetSettingsAsync(application);
if (settings.TryGetValue(Settings.TokenLifetimes.RequestToken, out string? setting) &&
TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value))
{
lifetime = value;
}
}
// Otherwise, fall back to the global value.
lifetime ??= context.Options.RequestTokenLifetime;
if (lifetime.HasValue)
{
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
}
// Use the server identity as the token issuer.
principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
// Store the type of the request token.
principal.SetClaim(Claims.Private.RequestTokenType, context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization => RequestTokenTypes.Private.CachedAuthorizationRequest,
OpenIddictServerEndpointType.EndSession => RequestTokenTypes.Private.CachedEndSessionRequest,
OpenIddictServerEndpointType.PushedAuthorization => RequestTokenTypes.Private.PushedAuthorizationRequest,
_ => null
});
// Store the request parameters as a special JSON object claim.
//
// Note: parameters used for client authentication are deliberately filtered out.
var parameters = from parameter in context.Request.GetParameters()
where parameter.Key is not (Parameters.ClientAssertion or
Parameters.ClientAssertionType or
Parameters.ClientSecret)
select parameter;
principal.SetClaim(Claims.Private.RequestParameters, JsonSerializer.Deserialize<JsonElement>(
JsonSerializer.Serialize(new OpenIddictRequest(parameters))));
context.RequestTokenPrincipal = principal;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the refresh token, if one is going to be returned.
@ -3238,7 +3616,7 @@ public static partial class OpenIddictServerHandlers
new PrepareRefreshTokenPrincipal(provider.GetService<IOpenIddictApplicationManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(PrepareDeviceCodePrincipal.Descriptor.Order + 1_000)
.SetOrder(PrepareRequestTokenPrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -3484,8 +3862,10 @@ public static partial class OpenIddictServerHandlers
// See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
principal.SetClaim(Claims.Nonce, context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization => context.Request.Nonce,
OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce),
OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.PushedAuthorization => context.Request.Nonce,
OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce),
_ => null
});
@ -3794,7 +4174,7 @@ public static partial class OpenIddictServerHandlers
},
Principal = context.DeviceCodePrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.DeviceCode,
TokenType = TokenTypeHints.DeviceCode
};
await _dispatcher.DispatchAsync(notification);
@ -3824,6 +4204,73 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for generating a request token for the current sign-in operation.
/// </summary>
public sealed class GenerateRequestToken : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
public GenerateRequestToken(IOpenIddictServerDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireRequestTokenGenerated>()
.UseScopedHandler<GenerateRequestToken>()
.SetOrder(GenerateDeviceCode.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new GenerateTokenContext(context.Transaction)
{
ClientId = context.ClientId,
CreateTokenEntry = !context.Options.DisableTokenStorage,
PersistTokenPayload = !context.Options.DisableTokenStorage,
IsReferenceToken = !context.Options.DisableTokenStorage,
Principal = context.RequestTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.Private.RequestToken
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
context.RequestToken = notification.Token;
}
}
/// <summary>
/// Contains the logic responsible for generating a refresh token for the current sign-in operation.
/// </summary>
@ -3841,7 +4288,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireRefreshTokenGenerated>()
.UseScopedHandler<GenerateRefreshToken>()
.SetOrder(GenerateDeviceCode.Descriptor.Order + 1_000)
.SetOrder(GenerateRequestToken.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -4368,6 +4815,17 @@ public static partial class OpenIddictServerHandlers
context.Response.IdToken = context.IdentityToken;
}
if (context.IncludeRequestToken)
{
if (context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession)
{
context.Response[Parameters.ClientId] = context.Request.ClientId;
}
context.Response.RequestUri = RequestUris.Prefixes.Generic + context.RequestToken;
}
if (context.IncludeRefreshToken)
{
context.Response.RefreshToken = context.RefreshToken;
@ -4417,6 +4875,29 @@ public static partial class OpenIddictServerHandlers
};
}
else if (context.EndpointType is OpenIddictServerEndpointType.PushedAuthorization)
{
context.Response.ExpiresIn = context.RequestTokenPrincipal?.GetExpirationDate() switch
{
// If an expiration date was set on the pushed authorization
// request token principal, return it to the client application.
DateTimeOffset date when date > (
#if SUPPORTS_TIME_PROVIDER
context.Options.TimeProvider?.GetUtcNow() ??
#endif
DateTimeOffset.UtcNow)
=> (long) ((date - (
#if SUPPORTS_TIME_PROVIDER
context.Options.TimeProvider?.GetUtcNow() ??
#endif
DateTimeOffset.UtcNow)).TotalSeconds + .5),
// Otherwise, return an arbitrary value, as the "expires_in"
// parameter is required in pushed authorization responses.
_ => 5 * 60 // 5 minutes, in seconds.
};
}
return default;
}
}
@ -4490,6 +4971,72 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to the received token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class RedeemLogoutTokenEntry : IOpenIddictServerHandler<ProcessSignOutContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RedeemLogoutTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public RedeemLogoutTokenEntry(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireEndSessionRequest>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<RedeemLogoutTokenEntry>()
// Note: this handler is deliberately executed early in the pipeline to ensure
// that the token database entry is always marked as redeemed even if the sign-out
// demand is rejected later in the pipeline (e.g because an error was returned).
.SetOrder(ValidateSignOutDemand.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = context.Transaction.GetProperty<ProcessAuthenticationContext>(
typeof(ProcessAuthenticationContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
var principal = notification.RequestTokenPrincipal;
if (principal is null)
{
return;
}
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
var identifier = principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
return;
}
// Mark the token as redeemed to prevent future reuses.
await _tokenManager.TryRedeemAsync(token);
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters
/// populated from user-defined handlers to the sign-out response.

34
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -94,6 +94,11 @@ public sealed class OpenIddictServerOptions
new Uri(".well-known/jwks", UriKind.Relative)
];
/// <summary>
/// Gets the absolute and relative URIs associated to the pushed authorization endpoint.
/// </summary>
public List<Uri> PushedAuthorizationEndpointUris { get; } = [];
/// <summary>
/// Gets the absolute and relative URIs associated to the revocation endpoint.
/// </summary>
@ -205,6 +210,13 @@ public sealed class OpenIddictServerOptions
/// </summary>
public TimeSpan? IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(20);
/// <summary>
/// Gets or sets the period of time request tokens remain valid after being issued. The default value is 1 hour.
/// The client application is expected to start a whole new authentication flow after the request token has expired.
/// While not recommended, this property can be set to <see langword="null"/> to issue request tokens that never expire.
/// </summary>
public TimeSpan? RequestTokenLifetime { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Gets or sets the period of time refresh tokens remain valid after being issued. The default value is 14 days.
/// The client application is expected to start a whole new authentication flow after the refresh token has expired.
@ -338,6 +350,20 @@ public sealed class OpenIddictServerOptions
/// </summary>
public bool DisableScopeValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether requests received by the authorization
/// endpoint should be stored in the token store, which allows flowing
/// large payloads across requests. Enabling this option can be useful
/// for clients that do not supported pushed authorization requests.
/// </summary>
public bool EnableAuthorizationRequestCaching { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether requests received
/// by the end session endpoint should be stored in the token store.
/// </summary>
public bool EnableEndSessionRequestCaching { get; set; }
/// <summary>
/// Gets the OAuth 2.0 client assertion types enabled for this application.
/// </summary>
@ -396,6 +422,14 @@ public sealed class OpenIddictServerOptions
/// </summary>
public bool RequireProofKeyForCodeExchange { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether pushed authorization requests must be used
/// by client applications when using an interactive flow like the authorization code or
/// implicit flows. If this property is set to <see langword="true"/>, authorization requests
/// that don't contain a request_uri parameter will be automatically rejected by OpenIddict.
/// </summary>
public bool RequirePushedAuthorizationRequests { get; set; }
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect response types enabled for this application.
/// </summary>

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

@ -14,7 +14,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
<PackageReference Include="Microsoft.Owin.Security" />
</ItemGroup>

7
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictRequestTests.cs

@ -226,13 +226,6 @@ public class OpenIddictRequestTests
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
};
yield return new object[]
{
/* property: */ nameof(OpenIddictRequest.RequestId),
/* name: */ Parameters.RequestId,
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
};
yield return new object[]
{
/* property: */ nameof(OpenIddictRequest.RequestUri),

7
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs

@ -85,6 +85,13 @@ public class OpenIddictResponseTests
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
};
yield return new object[]
{
/* property: */ nameof(OpenIddictResponse.RequestUri),
/* name: */ Parameters.RequestUri,
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
};
yield return new object[]
{
/* property: */ nameof(OpenIddictResponse.Scope),

57
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Authentication.cs

@ -1,57 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Server.IntegrationTests;
using Xunit;
namespace OpenIddict.Server.AspNetCore.IntegrationTests;
public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests
{
[Fact(Skip = "The handler responsible for rejecting such requests has not been ported yet.")]
public async Task ExtractAuthorizationRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2028(Parameters.RequestId), response.ErrorDescription);
}
[Fact]
public async Task ExtractAuthorizationRequest_InvalidRequestIdParameterIsRejected()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.Services.AddDistributedMemoryCache();
options.UseAspNetCore()
.EnableAuthorizationRequestCaching();
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2052(Parameters.RequestId), response.ErrorDescription);
}
}

57
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Session.cs

@ -1,57 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Server.IntegrationTests;
using Xunit;
namespace OpenIddict.Server.AspNetCore.IntegrationTests;
public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests
{
[Fact(Skip = "The handler responsible for rejecting such requests has not been ported yet.")]
public async Task ExtractEndSessionRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/endsession", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2028(Parameters.RequestId), response.ErrorDescription);
}
[Fact]
public async Task ExtractEndSessionRequest_InvalidRequestIdParameterIsRejected()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.Services.AddDistributedMemoryCache();
options.UseAspNetCore()
.EnableEndSessionRequestCaching();
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/endsession", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2052(Parameters.RequestId), response.ErrorDescription);
}
}

2582
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs

File diff suppressed because it is too large

66
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs

@ -317,6 +317,7 @@ public abstract partial class OpenIddictServerIntegrationTests
.SetDeviceAuthorizationEndpointUris("path/device_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetEndSessionEndpointUris("path/logout_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint")
.SetUserInfoEndpointUris("path/userinfo_endpoint");
@ -333,6 +334,7 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("http://localhost/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("http://localhost/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]);
Assert.Equal("http://localhost/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("http://localhost/path/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("http://localhost/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("http://localhost/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]);
Assert.Equal("http://localhost/path/userinfo_endpoint", (string?) response[Metadata.UserInfoEndpoint]);
@ -349,6 +351,7 @@ public abstract partial class OpenIddictServerIntegrationTests
.SetDeviceAuthorizationEndpointUris("path/device_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetEndSessionEndpointUris("path/logout_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint")
.SetUserInfoEndpointUris("path/userinfo_endpoint");
@ -377,6 +380,7 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("https://contoso.com/issuer/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/userinfo_endpoint", (string?) response[Metadata.UserInfoEndpoint]);
@ -556,6 +560,48 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Contains("custom", methods);
}
[Fact]
public async Task HandleConfigurationRequest_NoClientAuthenticationMethodIsIncludedWhenPushedAuthorizationEndpointIsDisabled()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.SetPushedAuthorizationEndpointUris(Array.Empty<Uri>());
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/.well-known/openid-configuration");
// Assert
Assert.False(response.HasParameter(Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported));
}
[Fact]
public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenPushedAuthorizationEndpointIsEnabled()
{
// Arrange
await using var server = await CreateServerAsync(options => options.Configure(options =>
{
options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic);
options.ClientAuthenticationMethods.Add("custom");
}));
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/.well-known/openid-configuration");
var methods = (string[]?) response[Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported];
// Assert
Assert.NotNull(methods);
Assert.Equal(3, methods.Length);
Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods);
Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods);
Assert.Contains("custom", methods);
}
[Fact]
public async Task HandleConfigurationRequest_ConfiguredGrantTypesAreReturned()
{
@ -930,6 +976,26 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.False((bool?) response[Metadata.RequestUriParameterSupported]);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task HandleConfigurationRequest_PushedAuthorizationRequestsRequirementIsReflected(bool value)
{
// Arrange
await using var server = await CreateServerAsync(options => options.Configure(options =>
{
options.RequirePushedAuthorizationRequests = value;
}));
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/.well-known/openid-configuration");
// Assert
Assert.Equal(value, (bool?) response[Metadata.RequirePushedAuthorizationRequests]);
}
[Theory]
[InlineData("custom_error", null, null)]
[InlineData("custom_error", "custom_description", null)]

77
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs

@ -131,12 +131,87 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Magnifique", (string?) response["name"]);
}
[Fact]
public async Task ValidateEndSessionRequest_UnsupportedRequestUriIsRejected()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/connect/endsession", new OpenIddictRequest
{
RequestUri = "http://www.fabrikam.com/request/GkurKxf5T0Y-mnPFCHqWOMiZi4VS138cQO_V7PZHAdM"
});
// Assert
Assert.Equal(Errors.RequestUriNotSupported, response.Error);
Assert.Equal(SR.FormatID2028(Parameters.RequestUri), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2028), response.ErrorUri);
}
[Fact]
public async Task ValidateEndSession_ValidRequestUriDoesNotCauseAnError()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.AddEventHandler<HandleEndSessionRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SignOut();
return default;
}));
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("6esc_11ACC5bwc014ltc14eY22c", context.Token);
Assert.Equal([TokenTypeHints.Private.RequestToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.Private.RequestToken)
.SetClaim(Claims.Private.RequestTokenType, RequestTokenTypes.Private.CachedEndSessionRequest)
.SetClaim(Claims.Private.RequestParameters, $$"""
{
"post_logout_redirect_uri": "http://www.fabrikam.com/path",
"state": "af0ifjsldkj"
}
""");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/connect/endsession", new OpenIddictRequest
{
ClientId = "Fabrikam",
RequestUri = RequestUris.Prefixes.Generic + "6esc_11ACC5bwc014ltc14eY22c"
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.Equal("af0ifjsldkj", response.State);
}
[Theory]
[InlineData("/path", SR.ID2030)]
[InlineData("/tmp/file.xml", SR.ID2030)]
[InlineData("C:\\tmp\\file.xml", SR.ID2030)]
[InlineData("http://www.fabrikam.com/path#param=value", SR.ID2031)]
public async Task ValidateEndSessionRequest_InvalidRedirectUriCausesAnError(string uri, string message)
public async Task ValidateEndSessionRequest_InvalidPostLogoutRedirectUriCausesAnError(string uri, string message)
{
// Arrange
await using var server = await CreateServerAsync();

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

@ -98,6 +98,14 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData("/.WELL-KNOWN/JWKS/SUBPATH", OpenIddictServerEndpointType.Unknown)]
[InlineData("/.well-known/jwks/subpath/", OpenIddictServerEndpointType.Unknown)]
[InlineData("/.WELL-KNOWN/JWKS/SUBPATH/", OpenIddictServerEndpointType.Unknown)]
[InlineData("/connect/par", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("/CONNECT/PAR", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("/connect/par/", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("/CONNECT/PAR/", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("/connect/par/subpath", OpenIddictServerEndpointType.Unknown)]
[InlineData("/CONNECT/PAR/SUBPATH", OpenIddictServerEndpointType.Unknown)]
[InlineData("/connect/par/subpath/", OpenIddictServerEndpointType.Unknown)]
[InlineData("/CONNECT/PAR/SUBPATH/", OpenIddictServerEndpointType.Unknown)]
[InlineData("/connect/revoke", OpenIddictServerEndpointType.Revocation)]
[InlineData("/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)]
[InlineData("/connect/revoke/", OpenIddictServerEndpointType.Revocation)]
@ -284,6 +292,22 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)]
[InlineData("https://localhost:8888/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)]
[InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)]
[InlineData("https://localhost/connect/par", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("HTTPS://LOCALHOST/CONNECT/PAR", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("https://localhost/connect/par/", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("HTTPS://LOCALHOST/CONNECT/PAR/", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("https://localhost:443/connect/par", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("HTTPS://LOCALHOST:443/CONNECT/PAR", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("https://localhost:443/connect/par/", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("HTTPS://LOCALHOST:443/CONNECT/PAR/", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("https://fabrikam.com/connect/par", OpenIddictServerEndpointType.Unknown)]
[InlineData("HTTPS://FABRIKAM.COM/CONNECT/PAR", OpenIddictServerEndpointType.Unknown)]
[InlineData("https://fabrikam.com/connect/par/", OpenIddictServerEndpointType.Unknown)]
[InlineData("HTTPS://FABRIKAM.COM/CONNECT/PAR/", OpenIddictServerEndpointType.Unknown)]
[InlineData("https://localhost:8888/connect/par", OpenIddictServerEndpointType.Unknown)]
[InlineData("HTTPS://LOCALHOST:8888/CONNECT/PAR", OpenIddictServerEndpointType.Unknown)]
[InlineData("https://localhost:8888/connect/par/", OpenIddictServerEndpointType.Unknown)]
[InlineData("HTTPS://LOCALHOST:8888/CONNECT/PAR/", OpenIddictServerEndpointType.Unknown)]
[InlineData("https://localhost/connect/revoke", OpenIddictServerEndpointType.Revocation)]
[InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)]
[InlineData("https://localhost/connect/revoke/", OpenIddictServerEndpointType.Revocation)]
@ -345,6 +369,7 @@ public abstract partial class OpenIddictServerIntegrationTests
.SetDeviceAuthorizationEndpointUris("https://localhost/connect/device")
.SetIntrospectionEndpointUris("https://localhost/connect/introspect")
.SetEndSessionEndpointUris("https://localhost/connect/endsession")
.SetPushedAuthorizationEndpointUris("https://localhost/connect/par")
.SetRevocationEndpointUris("https://localhost/connect/revoke")
.SetTokenEndpointUris("https://localhost/connect/token")
.SetUserInfoEndpointUris("https://localhost/connect/userinfo")
@ -390,6 +415,7 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData("/custom/connect/custom", OpenIddictServerEndpointType.Unknown)]
[InlineData("/custom/connect/introspect", OpenIddictServerEndpointType.Introspection)]
[InlineData("/custom/connect/endsession", OpenIddictServerEndpointType.EndSession)]
[InlineData("/custom/connect/par", OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData("/custom/connect/revoke", OpenIddictServerEndpointType.Revocation)]
[InlineData("/custom/connect/token", OpenIddictServerEndpointType.Token)]
[InlineData("/custom/connect/userinfo", OpenIddictServerEndpointType.UserInfo)]
@ -492,9 +518,9 @@ public abstract partial class OpenIddictServerIntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetEndSessionEndpointUris("/authenticate");
options.SetUserInfoEndpointUris("/authenticate");
options.AddEventHandler<HandleEndSessionRequestContext>(builder =>
options.AddEventHandler<HandleUserInfoRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
@ -522,9 +548,9 @@ public abstract partial class OpenIddictServerIntegrationTests
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetEndSessionEndpointUris("/authenticate");
options.SetUserInfoEndpointUris("/authenticate");
options.AddEventHandler<HandleEndSessionRequestContext>(builder =>
options.AddEventHandler<HandleUserInfoRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
@ -813,6 +839,210 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]);
}
[Fact]
public async Task ProcessAuthentication_RequestTokenPrincipalIsNotPopulatedWhenRequestTokenIsMissing()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetAuthorizationEndpointUris("/authenticate");
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
// Assert
Assert.Null(context.RequestTokenPrincipal);
return default;
});
builder.SetOrder(int.MaxValue);
});
});
await using var client = await server.CreateClientAsync();
// Act
await client.PostAsync("/authenticate", new OpenIddictRequest
{
ClientId = "Fabrikam",
Nonce = "n-0S6_WzA2Mj",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code,
Scope = Scopes.OpenId
});
}
[Fact]
public async Task ProcessAuthentication_RequestTokenPrincipalIsNotPopulatedWhenRequestTokenIsInvalid()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetAuthorizationEndpointUris("/authenticate");
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
// Assert
Assert.Null(context.RequestTokenPrincipal);
return default;
});
builder.SetOrder(int.MaxValue);
});
});
await using var client = await server.CreateClientAsync();
// Act
await client.PostAsync("/authenticate", new OpenIddictRequest
{
ClientId = "Fabrikam",
RequestUri = RequestUris.Prefixes.Generic + "request_token"
});
}
[Fact]
public async Task ProcessAuthentication_RequestTokenPrincipalIsPopulatedWhenRequestTokenTypeIsInvalid()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetAuthorizationEndpointUris("/authenticate");
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("request_token", context.Token);
Assert.Equal([TokenTypeHints.Private.RequestToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.Private.RequestToken)
.SetClaim(Claims.Private.RequestTokenType, RequestTokenTypes.Private.CachedEndSessionRequest);
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
// Assert
Assert.NotNull(context.RequestTokenPrincipal);
Assert.NotNull(context.RequestTokenPrincipal.GetClaim(Claims.Private.RequestParameters));
return default;
});
builder.SetOrder(int.MaxValue);
});
});
await using var client = await server.CreateClientAsync();
// Act
await client.PostAsync("/authenticate", new OpenIddictRequest
{
ClientId = "Fabrikam",
RequestUri = RequestUris.Prefixes.Generic + "request_token"
});
}
[Fact]
public async Task ProcessAuthentication_RequestTokenPrincipalIsPopulatedWhenRequestTokenIsValid()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetAuthorizationEndpointUris("/authenticate");
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("request_token", context.Token);
Assert.Equal([TokenTypeHints.Private.RequestToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.Private.RequestToken)
.SetClaim(Claims.Private.RequestTokenType, RequestTokenTypes.Private.PushedAuthorizationRequest)
.SetClaim(Claims.Private.RequestParameters, "{}");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
// Assert
Assert.NotNull(context.RequestTokenPrincipal);
Assert.NotNull(context.RequestTokenPrincipal.GetClaim(Claims.Private.RequestParameters));
return default;
});
builder.SetOrder(ValidateRequestTokenType.Descriptor.Order + 1);
});
});
await using var client = await server.CreateClientAsync();
// Act
await client.PostAsync("/authenticate", new OpenIddictRequest
{
ClientId = "Fabrikam",
RequestUri = RequestUris.Prefixes.Generic + "request_token"
});
}
[Fact]
public async Task ProcessAuthentication_MissingRefreshTokenReturnsNull()
{
@ -3885,6 +4115,7 @@ public abstract partial class OpenIddictServerIntegrationTests
.SetDeviceAuthorizationEndpointUris("/connect/device")
.SetIntrospectionEndpointUris("/connect/introspect")
.SetEndSessionEndpointUris("/connect/endsession")
.SetPushedAuthorizationEndpointUris("/connect/par")
.SetRevocationEndpointUris("/connect/revoke")
.SetTokenEndpointUris("/connect/token")
.SetUserInfoEndpointUris("/connect/userinfo")
@ -3921,12 +4152,18 @@ public abstract partial class OpenIddictServerIntegrationTests
options.AddEventHandler<ValidateAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context => default));
options.AddEventHandler<ValidateDeviceAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context => default));
options.AddEventHandler<ValidateEndSessionRequestContext>(builder =>
builder.UseInlineHandler(context => default));
options.AddEventHandler<ValidateEndUserVerificationRequestContext>(builder =>
builder.UseInlineHandler(context => default));
options.AddEventHandler<ValidatePushedAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context => default));
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
builder.UseInlineHandler(context => default));

57
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Authentication.cs

@ -1,57 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Server.IntegrationTests;
using Xunit;
namespace OpenIddict.Server.Owin.IntegrationTests;
public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests
{
[Fact(Skip = "The handler responsible for rejecting such requests has not been ported yet.")]
public async Task ExtractAuthorizationRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2028(Parameters.RequestId), response.ErrorDescription);
}
[Fact]
public async Task ExtractAuthorizationRequest_InvalidRequestIdParameterIsRejected()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.Services.AddDistributedMemoryCache();
options.UseOwin()
.EnableAuthorizationRequestCaching();
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2052(Parameters.RequestId), response.ErrorDescription);
}
}

57
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Session.cs

@ -1,57 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Server.IntegrationTests;
using Xunit;
namespace OpenIddict.Server.Owin.IntegrationTests;
public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests
{
[Fact(Skip = "The handler responsible for rejecting such requests has not been ported yet.")]
public async Task ExtractEndSessionRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/endsession", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2028(Parameters.RequestId), response.ErrorDescription);
}
[Fact]
public async Task ExtractEndSessionRequest_InvalidRequestIdParameterIsRejected()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.Services.AddDistributedMemoryCache();
options.UseOwin()
.EnableEndSessionRequestCaching();
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/endsession", new OpenIddictRequest
{
RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2052(Parameters.RequestId), response.ErrorDescription);
}
}
Loading…
Cancel
Save