diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
index 28bc8224..fb642c88 100644
--- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
+++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
@@ -225,6 +225,16 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
return Set(registration => registration.Scopes.UnionWith(scopes));
}
+ ///
+ /// 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.
+ ///
+ /// The instance.
+ public {{ provider.name }} DisablePushedAuthorizationRequests()
+ => Set(registration => registration.DisablePushedAuthorizationRequests = true);
+
///
/// Sets the issuer that will be attached to the
/// instances created by the OpenIddict client stack for this provider.
diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
index 36c3a9c3..4e5e5bdf 100644
--- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
@@ -48,6 +48,11 @@ public class AuthorizationController : Controller
[HttpGet, Route("~/connect/authorize")]
public async Task 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 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.");
diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
index ad53f978..c1b2ea00 100644
--- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
+++ b/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
}
});
}
diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
index 0b1f2308..d3ee0f81 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
@@ -58,6 +58,11 @@ public class AuthorizationController : Controller
[IgnoreAntiforgeryToken]
public async Task 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 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.");
diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
index 040a48c5..a73a7809 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
+++ b/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()
diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
index f4d44f2f..7d79458c 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
+++ b/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
}
});
}
diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index ed71631b..5c2699b3 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/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
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 25987cc0..d1659638 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/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.
- The specified principal contains an authenticated identity, which is not valid when the sign-in operation is triggered from the device authorization endpoint.
+ 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'.
- The specified principal contains a subject claim, which is not valid when the sign-in operation is triggered from the device authorization endpoint.
+ 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.
The specified principal doesn't contain a valid/authenticated identity.
@@ -218,8 +218,8 @@ Alternatively, create a class implementing 'IOpenIddictServerHandler<HandleAu
To apply authorization responses, create a class implementing 'IOpenIddictServerHandler<ApplyAuthorizationResponseContext>' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.
- The device request was not correctly extracted.
-To extract device requests, create a class implementing 'IOpenIddictServerHandler<ExtractDeviceAuthorizationRequestContext>' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.
+ The device authorization request was not correctly extracted.
+To extract device authorization requests, create a class implementing 'IOpenIddictServerHandler<ExtractDeviceAuthorizationRequestContext>' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.
The client application details cannot be found in the database.
@@ -416,7 +416,7 @@ To use key rollover, register both the new certificate and the old one in the cr
No custom authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateAuthorizationRequestContext>' must be implemented to validate authorization requests (e.g to ensure the client_id and redirect_uri are valid).
- No custom device request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateDeviceAuthorizationRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate device requests (e.g to ensure the client_id and client_secret are valid).
+ No custom device authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateDeviceAuthorizationRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate device authorization requests (e.g to ensure the client_id and client_secret are valid).
No custom introspection request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateIntrospectionRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate introspection requests (e.g to ensure the client_id and client_secret are valid).
@@ -501,12 +501,15 @@ This may indicate that the event handler responsible for processing OpenID Conne
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'.
+ This resource is no longer used and will be removed in a future version.
The authorization request payload is malformed.
+ This resource is no longer used and will be removed in a future version.
The end session request payload is malformed.
+ This resource is no longer used and will be removed in a future version.
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
A token must be specified when using revocation.
+
+ The authorization server requires using pushed authorization requests. Consider setting 'OpenIddictClientRegistration.DisablePushedAuthorizationRequests' to false to allow the OpenIddict client to use pushed authorization requests.
+
+
+ An error occurred while preparing the device authorization request.
+ Error: {0}
+ Error description: {1}
+ Error URI: {2}
+
+
+ An error occurred while sending the device authorization request.
+ Error: {0}
+ Error description: {1}
+ Error URI: {2}
+
+
+ An error occurred while extracting the device authorization response.
+ Error: {0}
+ Error description: {1}
+ Error URI: {2}
+
+
+ An error occurred while handling the device authorization response.
+ Error: {0}
+ Error description: {1}
+ Error URI: {2}
+
+
+ Authorization request caching and end session request caching cannot be used when disabling token storage.
+
+
+ No custom pushed authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidatePushedAuthorizationRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate pushed authorization requests (e.g to ensure the client_id and client_secret are valid).
+
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.
+
+ The pushed authorization request was not correctly extracted.
+To extract pushed authorization requests, create a class implementing 'IOpenIddictServerHandler<ExtractPushedAuthorizationRequestContext>' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.
+
+
+ The pushed authorization response was not correctly applied.
+To apply pushed authorization responses, create a class implementing 'IOpenIddictServerHandler<ApplyPushedAuthorizationResponseContext>' and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.
+
The security token is missing.
@@ -2134,7 +2178,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
The client application is not allowed to use the specified identity token hint.
- The specified state token is not suitable for the requested operation.
+ The specified '{0}' parameter is not suitable for the requested operation.
An unsupported content encoding was returned by the remote server.
@@ -2238,6 +2282,27 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
The introspection response indicates the token is no longer valid.
+
+ The '{0}' parameter must be attached as a regular OAuth 2.0 parameter when using a request object or pushed authorization requests.
+
+
+ The '{0}' parameter doesn't match the value specified in the request object or pushed authorization request.
+
+
+ The pushed authorization request was rejected by the remote server.
+
+
+ The mandatory '{0}' parameter couldn't be found in the pushed authorization response.
+
+
+ The '{0}' parameter returned in the pushed authorization response is not valid absolute URI.
+
+
+ A '{0}' obtained from the pushed authorization request endpoint is required for this client application.
+
+
+ This client application is not allowed to use the authorization endpoint.
+
The '{0}' parameter shouldn't be null or empty at this point.
@@ -2424,19 +2489,19 @@ The principal used to create the token contained the following claims: {Claims}.
The request URI matched a server endpoint: {Endpoint}.
- The device request was successfully extracted: {Request}.
+ The device authorization request was successfully extracted: {Request}.
- The device request was successfully validated.
+ The device authorization request was successfully validated.
- The device request was rejected because invalid scopes were specified: {Scopes}.
+ The device authorization request was rejected because invalid scopes were specified: {Scopes}.
- The device request was rejected because the application '{ClientId}' was not allowed to use the device authorization endpoint.
+ The device authorization request was rejected because the application '{ClientId}' was not allowed to use the device authorization endpoint.
- The device request was rejected because the application '{ClientId}' was not allowed to use the scope {Scope}.
+ The device authorization request was rejected because the application '{ClientId}' was not allowed to use the scope {Scope}.
The verification request was successfully extracted: {Request}.
@@ -2562,13 +2627,13 @@ The principal used to create the token contained the following claims: {Claims}.
The revocation request was rejected because the received token was of an unsupported type.
- The device request was rejected because the application '{ClientId}' was not allowed to use the device authorization flow.
+ The device authorization request was rejected because the application '{ClientId}' was not allowed to use the device authorization flow.
The revocation request was rejected because the access token was issued to a different client or for another resource server.
- The device request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.
+ The device authorization request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.
The revocation request was rejected because the refresh token was issued to a different client.
@@ -2901,6 +2966,96 @@ This may indicate that the hashed entry is corrupted or malformed.
The authorization request was rejected because an unsupported prompt parameter was specified.
+
+ The pushed authorization request was rejected by the remote authorization server: {Response}.
+
+
+ The pushed authorization request was successfully sent to {Uri}: {Request}.
+
+
+ The pushed authorization response returned by {Uri} was successfully extracted: {Response}.
+
+
+ The pushed authorization request was successfully extracted: {Request}.
+
+
+ The pushed authorization request was successfully validated.
+
+
+ The pushed authorization request was rejected because it contained an unsupported parameter: {Parameter}.
+
+
+ The pushed authorization request was rejected because the mandatory '{Parameter}' parameter was missing.
+
+
+ The pushed authorization request was rejected because the '{Parameter}' parameter wasn't a valid absolute URI: {RedirectUri}.
+
+
+ The pushed authorization request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.
+
+
+ The pushed authorization request was rejected because the '{ResponseType}' response type is not supported.
+
+
+ The pushed authorization request was rejected because the 'response_type'/'response_mode' combination was invalid: {ResponseType} ; {ResponseMode}.
+
+
+ The pushed authorization request was rejected because the '{ResponseMode}' response mode is not supported.
+
+
+ The pushed authorization request was rejected because the '{Scope}' scope was missing.
+
+
+ The pushed authorization request was rejected because an invalid prompt combination was specified.
+
+
+ The pushed authorization request was rejected because the specified code challenge method was not supported.
+
+
+ The pushed authorization request was rejected because the response type was not compatible with 'code_challenge'/'code_challenge_method'.
+
+
+ The pushed authorization request was rejected because the specified response type was not compatible with PKCE.
+
+
+ The pushed authorization request was rejected because the confidential application '{ClientId}' was not allowed to retrieve an access token from the authorization endpoint.
+
+
+ The pushed authorization request was rejected because the redirect_uri was invalid: '{RedirectUri}'.
+
+
+ The authentication request was rejected because invalid scopes were specified: {Scopes}.
+
+
+ The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the pushed authorization endpoint.
+
+
+ The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the authorization code flow.
+
+
+ The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the implicit flow.
+
+
+ The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the hybrid flow.
+
+
+ The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the '{Scope}' scope.
+
+
+ The pushed authorization request was rejected because the '{Parameter}' contained a forbidden parameter: {Name}.
+
+
+ The pushed authorization request was rejected because the '{ResponseType}' response type is not a valid combination.
+
+
+ The pushed authorization request was rejected because an unsupported prompt parameter was specified.
+
+
+ The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the '{ResponseType}' response type.
+
+
+ The pushed authorization request was rejected because the identity token used as a hint was issued to a different client.
+
https://documentation.openiddict.com/errors/{0}
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
index 11cbc21c..aef5c490 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
+++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
@@ -86,6 +86,11 @@ public sealed class OpenIddictConfiguration
///
public Uri? MtlsIntrospectionEndpoint { get; set; }
+ ///
+ /// Gets or sets the URI of the mTLS-enabled pushed authorization endpoint.
+ ///
+ public Uri? MtlsPushedAuthorizationEndpoint { get; set; }
+
///
/// Gets or sets the URI of the mTLS-enabled revocation endpoint.
///
@@ -106,6 +111,21 @@ public sealed class OpenIddictConfiguration
///
public Dictionary Properties { get; } = new(StringComparer.Ordinal);
+ ///
+ /// Gets or sets the URI of the pushed authorization endpoint.
+ ///
+ public Uri? PushedAuthorizationEndpoint { get; set; }
+
+ ///
+ /// Gets the client authentication methods supported by the pushed authorization endpoint.
+ ///
+ public HashSet PushedAuthorizationEndpointAuthMethodsSupported { get; } = new(StringComparer.Ordinal);
+
+ ///
+ /// Gets or sets a boolean indicating whether pushed authorization requests are required.
+ ///
+ public bool? RequirePushedAuthorizationRequests { get; set; }
+
///
/// Gets the response mode supported by the server.
///
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
index e5215cd6..1f983d21 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
+++ b/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);
+ ///
+ /// Gets the request token lifetime associated with the claims identity.
+ ///
+ /// The claims identity.
+ /// The request token lifetime or if the claim cannot be found.
+ public static TimeSpan? GetRequestTokenLifetime(this ClaimsIdentity identity)
+ => GetLifetime(identity, Claims.Private.RequestTokenLifetime);
+
+ ///
+ /// Gets the request token lifetime associated with the claims principal.
+ ///
+ /// The claims principal.
+ /// The request token lifetime or if the claim cannot be found.
+ public static TimeSpan? GetRequestTokenLifetime(this ClaimsPrincipal principal)
+ => GetLifetime(principal, Claims.Private.RequestTokenLifetime);
+
///
/// Gets the refresh token lifetime associated with the claims identity.
///
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs
index c3a152ca..e6befbf8 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs
+++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs
@@ -375,6 +375,7 @@ public class OpenIddictRequest : OpenIddictMessage
///
/// Gets or sets the "request_id" parameter.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public string? RequestId
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.RequestId);
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
index 0e78008d..70431cc2 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
+++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
@@ -198,6 +198,15 @@ public class OpenIddictResponse : OpenIddictMessage
set => SetParameter(OpenIddictConstants.Parameters.RefreshToken, value);
}
+ ///
+ /// Gets or sets the "request_uri" parameter.
+ ///
+ public string? RequestUri
+ {
+ get => (string?) GetParameter(OpenIddictConstants.Parameters.RequestUri);
+ set => SetParameter(OpenIddictConstants.Parameters.RequestUri, value);
+ }
+
///
/// Gets or sets the "scope" parameter.
///
diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj b/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj
index fba06f9e..03f55a73 100644
--- a/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj
+++ b/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj
@@ -24,7 +24,6 @@
('$(TargetFrameworkIdentifier)' == '.NETStandard') ">
-
diff --git a/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj b/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj
index b05d3a81..b6937f17 100644
--- a/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj
+++ b/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj
@@ -14,7 +14,6 @@
-
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Authorization.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Authorization.cs
new file mode 100644
index 00000000..2b447bd1
--- /dev/null
+++ b/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 DefaultHandlers { get; } = ImmutableArray.Create([
+ /*
+ * Pushed authorization request processing:
+ */
+ CreateHttpClient.Descriptor,
+ PreparePostHttpRequest.Descriptor,
+ AttachHttpVersion.Descriptor,
+ AttachJsonAcceptHeaders.Descriptor,
+ AttachUserAgentHeader.Descriptor,
+ AttachFromHeader.Descriptor,
+ AttachBasicAuthenticationCredentials.Descriptor,
+ AttachHttpParameters.Descriptor,
+ SendHttpRequest.Descriptor,
+ DisposeHttpRequest.Descriptor,
+
+ /*
+ * Pushed authorization response processing:
+ */
+ DecompressResponseContent.Descriptor,
+ ExtractJsonHttpResponse.Descriptor,
+ ExtractWwwAuthenticateHeader.Descriptor,
+ ValidateHttpResponse.Descriptor,
+ DisposeHttpResponse.Descriptor
+ ]);
+ }
+}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
index f848d329..8b594e3e 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
@@ -14,7 +14,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
{
public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create([
/*
- * DeviceAuthorization request processing:
+ * Device authorization request processing:
*/
CreateHttpClient.Descriptor,
PreparePostHttpRequest.Descriptor,
@@ -28,7 +28,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
DisposeHttpRequest.Descriptor,
/*
- * DeviceAuthorization response processing:
+ * Device authorization response processing:
*/
DecompressResponseContent.Descriptor,
ExtractJsonHttpResponse.Descriptor,
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
index 3625acaf..5bbeccc3 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
+++ b/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,
diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs
index 5599f07d..976c4a26 100644
--- a/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs
+++ b/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs
@@ -78,6 +78,119 @@ public static partial class OpenIddictClientEvents
public string AuthorizationEndpoint { get; set; } = null!;
}
+ ///
+ /// 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.
+ ///
+ public sealed class PreparePushedAuthorizationRequestContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public PreparePushedAuthorizationRequestContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+ }
+
+ ///
+ /// Represents an event called for each request to the pushed authorization endpoint request
+ /// to send the pushed authorization request to the remote authorization server.
+ ///
+ public sealed class ApplyPushedAuthorizationRequestContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ApplyPushedAuthorizationRequestContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+ }
+
+ ///
+ /// Represents an event called for each pushed authorization response
+ /// to extract the response parameters from the server response.
+ ///
+ public sealed class ExtractPushedAuthorizationResponseContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ExtractPushedAuthorizationResponseContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the response, or if it wasn't extracted yet.
+ ///
+ public OpenIddictResponse? Response
+ {
+ get => Transaction.Response;
+ set => Transaction.Response = value;
+ }
+ }
+
+ ///
+ /// Represents an event called for each pushed authorization response.
+ ///
+ public sealed class HandlePushedAuthorizationResponseContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public HandlePushedAuthorizationResponseContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the response.
+ ///
+ public OpenIddictResponse Response
+ {
+ get => Transaction.Response!;
+ set => Transaction.Response = value;
+ }
+ }
+
///
/// 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.
diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs
index 84041ace..e7a93c66 100644
--- a/src/OpenIddict.Client/OpenIddictClientEvents.cs
+++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs
@@ -1052,6 +1052,17 @@ public static partial class OpenIddictClientEvents
///
public string? DeviceAuthorizationEndpointClientAuthenticationMethod { get; set; }
+ ///
+ /// Gets or sets the URI of the pushed authorization endpoint, if applicable.
+ ///
+ public Uri? PushedAuthorizationEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the client authentication method used when communicating
+ /// with the pushed authorization endpoint, if applicable.
+ ///
+ public string? PushedAuthorizationEndpointClientAuthenticationMethod { get; set; }
+
///
/// 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
///
public bool SendDeviceAuthorizationRequest { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether a pushed authorization request should be sent.
+ ///
+ public bool SendPushedAuthorizationRequest { get; set; }
+
///
/// 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
///
public OpenIddictResponse? DeviceAuthorizationResponse { get; set; }
+ ///
+ /// Gets or sets the request sent to the pushed authorization endpoint, if applicable.
+ ///
+ public OpenIddictRequest? PushedAuthorizationRequest { get; set; }
+
+ ///
+ /// Gets or sets the response returned by the pushed authorization endpoint, if applicable.
+ ///
+ public OpenIddictResponse? PushedAuthorizationResponse { get; set; }
+
///
/// 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
///
public bool ExtractDeviceCode { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether a request token
+ /// should be extracted from the current context.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool ExtractRequestToken { get; set; }
+
///
/// 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
///
public bool RequireDeviceCode { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether a request token
+ /// must be resolved for the authentication to be considered valid.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool RequireRequestToken { get; set; }
+
///
/// 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
///
public bool ValidateDeviceCode { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether the request token
+ /// extracted from the current context should be validated.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool ValidateRequestToken { get; set; }
+
///
/// 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
///
public bool RejectDeviceCode { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether an invalid request token
+ /// will cause the authentication demand to be rejected or will be ignored.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool RejectRequestToken { get; set; }
+
///
/// 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
///
public string? DeviceCode { get; set; }
+ ///
+ /// Gets or sets the request token to validate, if applicable.
+ ///
+ public string? RequestToken { get; set; }
+
///
/// Gets or sets the user code to validate, if applicable.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs
index 4bd5a913..dd5f4146 100644
--- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs
+++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs
@@ -56,6 +56,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
index e01710e0..98d6cbc8 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
@@ -371,6 +371,23 @@ public static class OpenIddictClientHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no pushed authorization request is expected to be sent.
+ ///
+ public sealed class RequirePushedAuthorizationRequest : IOpenIddictClientHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(ProcessChallengeContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.SendPushedAuthorizationRequest);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if the request is not a redirection request.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
index c278de6c..1cc346d6 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
+++ b/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
}
}
+ ///
+ /// Contains the logic responsible for validating the well-known parameters contained in the pushed authorization response.
+ ///
+ public sealed class ValidateWellKnownPushedAuthorizationResponseParameters : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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
+ };
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for surfacing potential errors from the pushed authorization response.
+ ///
+ public sealed class HandlePushedAuthorizationErrorResponse : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateWellKnownPushedAuthorizationResponseParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for validating the request URI contained in the pushed authorization response.
+ ///
+ public sealed class ValidatePushedAuthorizationRequestUri : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(HandlePushedAuthorizationErrorResponse.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for validating the "expires_in"
+ /// parameter contained in the pushed authorization response.
+ ///
+ public sealed class ValidatePushedAuthorizationExpiration : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedAuthorizationRequestUri.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// Contains the logic responsible for extracting redirection requests and invoking the corresponding event handlers.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Device.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Device.cs
index e3d04fe0..bc241523 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Device.cs
+++ b/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
]);
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
index 2bb8f9c8..3bf29d14 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
+++ b/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
}
}
+ ///
+ /// Contains the logic responsible for extracting the mTLS-enabled pushed authorization endpoint URI from the discovery document.
+ ///
+ public sealed class ExtractMtlsPushedAuthorizationEndpoint : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ExtractMtlsIntrospectionEndpoint.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// Contains the logic responsible for extracting the mTLS-enabled revocation endpoint URI from the discovery document.
///
@@ -575,7 +624,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .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
}
}
+ ///
+ /// Contains the logic responsible for extracting the pushed authorization endpoint URI from the discovery document.
+ ///
+ public sealed class ExtractPushedAuthorizationEndpoint : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ExtractEndSessionEndpoint.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// Contains the logic responsible for extracting the revocation endpoint URI from the discovery document.
///
@@ -698,7 +790,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .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
}
}
+ ///
+ /// Contains the logic responsible for extracting the flag indicating whether pushed
+ /// authorization requests (PAR) are considered mandatory from the discovery document.
+ ///
+ public sealed class ExtractPushedAuthorizationRequirement : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleConfigurationResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.Configuration.RequirePushedAuthorizationRequests = (bool?)
+ context.Response[Metadata.RequirePushedAuthorizationRequests];
+
+ return default;
+ }
+ }
+
///
/// 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()
.UseSingletonHandler()
- .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
}
}
+ ///
+ /// Contains the logic responsible for extracting the authentication methods
+ /// supported by the pushed authorization endpoint from the discovery document.
+ ///
+ public sealed class ExtractPushedAuthorizationEndpointClientAuthenticationMethods : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// 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()
.UseSingletonHandler()
- .SetOrder(ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor.Order + 1_000)
+ .SetOrder(ExtractPushedAuthorizationEndpointClientAuthenticationMethods.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index 5de69eb7..28318145 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/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
}
}
+ ///
+ /// Contains the logic responsible for determining whether a pushed authorization request should be sent.
+ ///
+ public sealed class EvaluatePushedAuthorizationRequest : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(AttachDeviceAuthorizationRequestParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for negotiating the best pushed authorization endpoint
+ /// client authentication method supported by both the client and the authorization server.
+ ///
+ public sealed class AttachPushedAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(EvaluatePushedAuthorizationRequest.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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,
+ _ => 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for resolving the URI of the pushed authorization endpoint.
+ ///
+ public sealed class ResolvePushedAuthorizationEndpoint : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for attaching the parameters to the pushed authorization request, if applicable.
+ ///
+ public sealed class AttachPushedAuthorizationRequestParameters : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ResolvePushedAuthorizationEndpoint.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// 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
}
}
+ ///
+ /// Contains the logic responsible for attaching the client credentials to the pushed authorization endpoint request, if applicable.
+ ///
+ public sealed class AttachPushedAuthorizationRequestClientCredentials : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidateRequiredDeviceAuthorizationTokens.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ public sealed class ValidatePushedAuthorizationRequirement : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(AttachPushedAuthorizationRequestClientCredentials.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for sending the pushed authorization endpoint request, if applicable.
+ ///
+ public sealed class SendPushedAuthorizationRequest : IOpenIddictClientHandler
+ {
+ private readonly OpenIddictClientService _service;
+
+ public SendPushedAuthorizationRequest(OpenIddictClientService service)
+ => _service = service ?? throw new ArgumentNullException(nameof(service));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedAuthorizationRequirement.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for determining the set of pushed authorization tokens to validate.
+ ///
+ public sealed class EvaluateValidatedPushedTokens : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(SendPushedAuthorizationRequest.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for resolving the pushed
+ /// tokens from the pushed authorization response, if applicable.
+ ///
+ public sealed class ResolveValidatedPushedTokens : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(EvaluateValidatedPushedTokens.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting challenge demands that lack required tokens.
+ ///
+ public sealed class ValidateRequiredPushedAuthorizationTokens : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ // 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();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for attaching the request token to the authorization request.
+ ///
+ public sealed class AttachRequestToken : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidateRequiredPushedAuthorizationTokens.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessChallengeContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.Request.RequestUri = context.RequestToken;
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for removing parameters that were sent as
+ /// part of the pushed authorization request from the authorization request.
+ ///
+ public sealed class RemovePushedAuthorizationRequestParameters : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachRequestToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// Contains the logic responsible for rejecting invalid introspection demands.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs
index 5e973812..48e6c8d4 100644
--- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs
+++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs
@@ -32,6 +32,15 @@ public sealed class OpenIddictClientRegistration
///
public string? ClientSecret { get; set; }
+ ///
+ /// 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 ,
+ /// an exception is automatically thrown when starting an interactive authentication challenge.
+ ///
+ public bool DisablePushedAuthorizationRequests { get; set; }
+
///
/// Gets or sets the URI of the redirection endpoint that will handle the callback.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs
index cad51c4e..7a7ca810 100644
--- a/src/OpenIddict.Client/OpenIddictClientService.cs
+++ b/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
}
}
+ ///
+ /// Sends the pushed authorization request and retrieves the corresponding response.
+ ///
+ /// The client registration.
+ /// The server configuration.
+ /// The pushed authorization request.
+ /// The uri of the remote pushed authorization endpoint.
+ /// The client authentication method, if applicable.
+ /// The that can be used to abort the operation.
+ /// The token response.
+ internal async ValueTask 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();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ var transaction = await factory.CreateTransactionAsync();
+
+ request = await PreparePushedAuthorizationRequestAsync();
+ request = await ApplyPushedAuthorizationRequestAsync();
+
+ var response = await ExtractPushedAuthorizationResponseAsync();
+
+ return await HandlePushedAuthorizationResponseAsync();
+
+ async ValueTask 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 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 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 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();
+ }
+ }
+ }
+
///
/// Sends the revocation request and retrieves the corresponding response.
///
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
index 9d408d37..3c485a49 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
+++ b/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.
///
/// The instance.
+ [Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder EnableAuthorizationRequestCaching()
- => Configure(options => options.EnableAuthorizationRequestCaching = true);
+ {
+ Services.Configure(options => options.EnableAuthorizationRequestCaching = true);
+
+ return this;
+ }
///
/// Enables end session request caching, so that end session requests
/// are automatically stored in the distributed cache.
///
/// The instance.
+ [Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder EnableEndSessionRequestCaching()
- => Configure(options => options.EnableEndSessionRequestCaching = true);
+ {
+ Services.Configure(options => options.EnableEndSessionRequestCaching = true);
+
+ return this;
+ }
///
/// Enables status code pages integration support. Once enabled, errors
@@ -174,6 +185,7 @@ public sealed class OpenIddictServerAspNetCoreBuilder
///
/// The caching policy.
/// The instance.
+ [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
///
/// The caching policy.
/// The instance.
+ [Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerAspNetCoreBuilder SetEndSessionRequestCachingPolicy(DistributedCacheEntryOptions policy)
{
if (policy is null)
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
index d8a8ced7..2d91a5ee 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
@@ -11,12 +11,14 @@ namespace OpenIddict.Server.AspNetCore;
///
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";
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
index 490f4cf0..a383b233 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
+++ b/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();
- builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+#pragma warning restore CS0618
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
index 2dc257d6..a60b08e7 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
@@ -178,12 +178,7 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler 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
/// Represents a filter that excludes the associated handlers if authorization request caching was not enabled.
///
+ [Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireAuthorizationRequestCachingEnabled : IOpenIddictServerHandlerFilter
{
private readonly IOptionsMonitor _options;
@@ -64,6 +65,7 @@ public static class OpenIddictServerAspNetCoreHandlerFilters
///
/// Represents a filter that excludes the associated handlers if end session request caching was not enabled.
///
+ [Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireEndSessionRequestCachingEnabled : IOpenIddictServerHandlerFilter
{
private readonly IOptionsMonitor _options;
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
index f203bfd3..570d1980 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
+++ b/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.Descriptor,
- RestoreCachedRequestParameters.Descriptor,
- CacheRequestParameters.Descriptor,
/*
* Authorization request handling:
@@ -44,29 +34,43 @@ public static partial class OpenIddictServerAspNetCoreHandlers
/*
* Authorization response processing:
*/
- RemoveCachedRequest.Descriptor,
AttachHttpResponseCode.Descriptor,
AttachCacheControlHeader.Descriptor,
+ ProcessSelfRedirection.Descriptor,
ProcessFormPostResponse.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessFragmentResponse.Descriptor,
ProcessPassthroughErrorResponse.Descriptor,
ProcessStatusCodePagesErrorResponse.Descriptor,
- ProcessLocalErrorResponse.Descriptor
+ ProcessLocalErrorResponse.Descriptor,
+
+ /*
+ * Pushed authorization request extraction:
+ */
+ ExtractPostRequest.Descriptor,
+ ValidateClientAuthenticationMethod.Descriptor,
+ ExtractBasicAuthenticationCredentials.Descriptor,
+
+ /*
+ * Pushed authorization response processing:
+ */
+ AttachHttpResponseCode.Descriptor,
+ AttachCacheControlHeader.Descriptor,
+ AttachWwwAuthenticateHeader.Descriptor,
+ ProcessJsonResponse.Descriptor
]);
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -81,92 +85,23 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler
{
- private readonly IDistributedCache _cache;
- private readonly IOptionsMonitor _options;
-
- public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
+ public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor options)
- {
- _cache = cache ?? throw new ArgumentNullException(nameof(cache));
- _options = options ?? throw new ArgumentNullException(nameof(options));
- }
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -181,97 +116,21 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -285,6 +144,28 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
+ ///
+ public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
+ }
+
+ ///
+ /// 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.
+ ///
+ public sealed class ProcessSelfRedirection : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(250_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
///
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()
.AddFilter()
.UseSingletonHandler()
- .SetOrder(250_000)
+ .SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
index b8e43285..ad4f82a7 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
+++ b/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.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.Descriptor,
AttachCacheControlHeader.Descriptor,
+ ProcessSelfRedirection.Descriptor,
+ ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse.Descriptor,
ProcessPassthroughErrorResponse.Descriptor,
ProcessStatusCodePagesErrorResponse.Descriptor,
ProcessLocalErrorResponse.Descriptor,
- ProcessQueryResponse.Descriptor,
ProcessEmptyResponse.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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -78,92 +67,23 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler
{
- private readonly IDistributedCache _cache;
- private readonly IOptionsMonitor _options;
-
- public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
+ public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor options)
- {
- _cache = cache ?? throw new ArgumentNullException(nameof(cache));
- _options = options ?? throw new ArgumentNullException(nameof(options));
- }
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -178,97 +98,21 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -282,6 +126,28 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
+ ///
+ public ValueTask HandleAsync(ApplyEndSessionResponseContext context)
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
+ }
+
+ ///
+ /// 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.
+ ///
+ public sealed class ProcessSelfRedirection : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(250_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
///
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()
.AddFilter()
.UseSingletonHandler()
- .SetOrder(250_000)
+ .SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
index 8f0423d4..32b60cfa 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
+++ b/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,
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs
index f14f7093..c294e9fe 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs
+++ b/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.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableAuthorizationRequestCaching { get; set; }
///
/// 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.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableEndSessionRequestCaching { get; set; }
///
@@ -106,6 +108,7 @@ public sealed class OpenIddictServerAspNetCoreOptions : AuthenticationSchemeOpti
///
/// Gets or sets the caching policy used by the authorization endpoint.
///
+ [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
///
/// Gets or sets the caching policy used by the end session endpoint.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public DistributedCacheEntryOptions EndSessionRequestCachingPolicy { get; set; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs
index 53bfae55..5a75d800 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs
+++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs
@@ -99,6 +99,13 @@ public sealed class OpenIddictServerDataProtectionBuilder
public OpenIddictServerDataProtectionBuilder PreferDefaultDeviceCodeFormat()
=> Configure(options => options.PreferDefaultDeviceCodeFormat = true);
+ ///
+ /// Configures OpenIddict to use the default token format (JWT) when issuing new request tokens.
+ ///
+ /// The instance.
+ public OpenIddictServerDataProtectionBuilder PreferDefaultRequestTokenFormat()
+ => Configure(options => options.PreferDefaultRequestTokenFormat = true);
+
///
/// Configures OpenIddict to use the default token format (JWT) when issuing new refresh tokens.
///
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
index 7a208bf4..7fe42b02 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
+++ b/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";
}
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
index 0505fb7e..57eb3a40 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
+++ b/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
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
index 1581687a..0e2de127 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
+++ b/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))
});
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs
index 027820a1..e267a78d 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs
+++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs
@@ -46,6 +46,12 @@ public sealed class OpenIddictServerDataProtectionOptions
///
public bool PreferDefaultDeviceCodeFormat { get; set; }
+ ///
+ /// 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 by default.
+ ///
+ public bool PreferDefaultRequestTokenFormat { get; set; }
+
///
/// 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 by default.
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
index 848b9cdf..dd209210 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
+++ b/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.
///
/// The instance.
+ [Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder EnableAuthorizationRequestCaching()
- => Configure(options => options.EnableAuthorizationRequestCaching = true);
+ {
+ Services.Configure(options => options.EnableAuthorizationRequestCaching = true);
+
+ return this;
+ }
///
/// Enables end session request caching, so that end session requests
/// are automatically stored in the distributed cache.
///
/// The instance.
+ [Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder EnableEndSessionRequestCaching()
- => Configure(options => options.EnableEndSessionRequestCaching = true);
+ {
+ Services.Configure(options => options.EnableEndSessionRequestCaching = true);
+
+ return this;
+ }
///
/// Suppresses indentation for the JSON responses returned by the OWIN host.
@@ -163,6 +174,7 @@ public sealed class OpenIddictServerOwinBuilder
///
/// The caching policy.
/// The instance.
+ [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
///
/// The caching policy.
/// The instance.
+ [Obsolete("This method is obsolete and will be removed in a future version.")]
public OpenIddictServerOwinBuilder SetEndSessionRequestCachingPolicy(DistributedCacheEntryOptions policy)
{
if (policy is null)
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
index 7f6ea11c..4d617422 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
@@ -11,6 +11,7 @@ namespace OpenIddict.Server.Owin;
///
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";
}
}
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
index 6a89f542..de3734b1 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
+++ b/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();
- builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+#pragma warning restore CS0618
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
index eb70557f..80f8392c 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
@@ -162,6 +162,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler 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
/// Represents a filter that excludes the associated handlers if authorization request caching was not enabled.
///
+ [Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireAuthorizationRequestCachingEnabled : IOpenIddictServerHandlerFilter
{
private readonly IOptionsMonitor _options;
@@ -62,6 +63,7 @@ public static class OpenIddictServerOwinHandlerFilters
///
/// Represents a filter that excludes the associated handlers if end session request caching was not enabled.
///
+ [Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireEndSessionRequestCachingEnabled : IOpenIddictServerHandlerFilter
{
private readonly IOptionsMonitor _options;
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
index 3b02908a..f7a7f7d3 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
+++ b/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.Descriptor,
- RestoreCachedRequestParameters.Descriptor,
- CacheRequestParameters.Descriptor,
/*
* Authorization request handling:
@@ -42,30 +33,46 @@ public static partial class OpenIddictServerOwinHandlers
/*
* Authorization response processing:
*/
- RemoveCachedRequest.Descriptor,
AttachHttpResponseCode.Descriptor,
AttachOwinResponseChallenge.Descriptor,
SuppressFormsAuthenticationRedirect.Descriptor,
AttachCacheControlHeader.Descriptor,
+ ProcessSelfRedirection.Descriptor,
ProcessFormPostResponse.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessFragmentResponse.Descriptor,
ProcessPassthroughErrorResponse.Descriptor,
- ProcessLocalErrorResponse.Descriptor
+ ProcessLocalErrorResponse.Descriptor,
+
+ /*
+ * Pushed authorization request extraction:
+ */
+ ExtractPostRequest.Descriptor,
+ ValidateClientAuthenticationMethod.Descriptor,
+ ExtractBasicAuthenticationCredentials.Descriptor,
+
+ /*
+ * Pushed authorization response processing:
+ */
+ AttachHttpResponseCode.Descriptor,
+ AttachOwinResponseChallenge.Descriptor,
+ SuppressFormsAuthenticationRedirect.Descriptor,
+ AttachCacheControlHeader.Descriptor,
+ AttachWwwAuthenticateHeader.Descriptor,
+ ProcessJsonResponse.Descriptor,
]);
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -80,92 +87,23 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler
{
- private readonly IDistributedCache _cache;
- private readonly IOptionsMonitor _options;
-
- public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
+ public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor options)
- {
- _cache = cache ?? throw new ArgumentNullException(nameof(cache));
- _options = options ?? throw new ArgumentNullException(nameof(options));
- }
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -180,97 +118,21 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -284,6 +146,28 @@ public static partial class OpenIddictServerOwinHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
+ ///
+ public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
+ }
+
+ ///
+ /// 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.
+ ///
+ public sealed class ProcessSelfRedirection : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(250_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
///
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()
.AddFilter()
.UseSingletonHandler()
- .SetOrder(250_000)
+ .SetOrder(ProcessSelfRedirection.Descriptor.Order)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
index 30cdc913..201497d5 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
+++ b/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.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.Descriptor,
AttachOwinResponseChallenge.Descriptor,
SuppressFormsAuthenticationRedirect.Descriptor,
AttachCacheControlHeader.Descriptor,
+ ProcessSelfRedirection.Descriptor,
+ ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse.Descriptor,
ProcessPassthroughErrorResponse.Descriptor,
ProcessLocalErrorResponse.Descriptor,
- ProcessQueryResponse.Descriptor,
ProcessEmptyResponse.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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RestoreCachedRequestParameters : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -78,92 +67,23 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class CacheRequestParameters : IOpenIddictServerHandler
{
- private readonly IDistributedCache _cache;
- private readonly IOptionsMonitor _options;
-
- public CacheRequestParameters() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0116));
+ public CacheRequestParameters() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
public CacheRequestParameters(
IDistributedCache cache,
IOptionsMonitor options)
- {
- _cache = cache ?? throw new ArgumentNullException(nameof(cache));
- _options = options ?? throw new ArgumentNullException(nameof(options));
- }
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -178,97 +98,21 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
///
- 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));
}
///
/// 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.
///
+ [Obsolete("This event handler is obsolete and will be removed in a future version.")]
public sealed class RemoveCachedRequest : IOpenIddictServerHandler
{
- 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));
///
/// Gets the default descriptor definition assigned to this handler.
@@ -282,6 +126,28 @@ public static partial class OpenIddictServerOwinHandlers
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
+ ///
+ public ValueTask HandleAsync(ApplyEndSessionResponseContext context)
+ => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
+ }
+
+ ///
+ /// 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.
+ ///
+ public sealed class ProcessSelfRedirection : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(250_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
///
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()
.AddFilter()
.UseSingletonHandler()
- .SetOrder(250_000)
+ .SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
index 28fe3bda..ca80f331 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
+++ b/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,
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs
index 24d33ee6..fd41e080 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs
+++ b/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.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableAuthorizationRequestCaching { get; set; }
///
/// 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.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public bool EnableEndSessionRequestCaching { get; set; }
///
@@ -103,6 +105,7 @@ public sealed class OpenIddictServerOwinOptions : AuthenticationOptions
///
/// Gets or sets the caching policy used by the authorization endpoint.
///
+ [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
///
/// Gets or sets the caching policy used by the end session endpoint.
///
+ [Obsolete("This property is obsolete and will be removed in a future version.")]
public DistributedCacheEntryOptions EndSessionRequestCachingPolicy { get; set; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs
index 85b1ca2d..94c1430a 100644
--- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs
+++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs
@@ -1385,6 +1385,55 @@ public sealed class OpenIddictServerBuilder
});
}
+ ///
+ /// 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.
+ ///
+ /// The URIs associated to the endpoint.
+ /// The instance.
+ 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());
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The URIs associated to the endpoint.
+ /// The instance.
+ 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);
+ });
+ }
+
///
/// 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
///
/// 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.
///
/// The instance.
public OpenIddictServerBuilder RequireProofKeyForCodeExchange()
=> Configure(options => options.RequireProofKeyForCodeExchange = true);
+ ///
+ /// 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.
+ ///
+ /// The instance.
+ public OpenIddictServerBuilder RequirePushedAuthorizationRequests()
+ => Configure(options => options.RequirePushedAuthorizationRequests = true);
+
///
/// 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);
+ ///
+ /// 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.
+ ///
+ /// The instance.
+ public OpenIddictServerBuilder EnableAuthorizationRequestCaching()
+ => Configure(options => options.EnableAuthorizationRequestCaching = true);
+
+ ///
+ /// Enables end session request storage, so that end session requests
+ /// are automatically stored in the token store, which allows flowing
+ /// large payloads across requests.
+ ///
+ /// The instance.
+ public OpenIddictServerBuilder EnableEndSessionRequestCaching()
+ => Configure(options => options.EnableEndSessionRequestCaching = true);
+
///
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
index b8caa78d..de9c9300 100644
--- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
+++ b/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
+ (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)) &&
diff --git a/src/OpenIddict.Server/OpenIddictServerEndpointType.cs b/src/OpenIddict.Server/OpenIddictServerEndpointType.cs
index e17a4ead..7104c171 100644
--- a/src/OpenIddict.Server/OpenIddictServerEndpointType.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEndpointType.cs
@@ -64,5 +64,10 @@ public enum OpenIddictServerEndpointType
///
/// User verification endpoint.
///
- EndUserVerification = 10
+ EndUserVerification = 10,
+
+ ///
+ /// Pushed authorization endpoint.
+ ///
+ PushedAuthorization = 11
}
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs
index 72a2e966..62cc115a 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs
@@ -69,7 +69,7 @@ public static partial class OpenIddictServerEvents
/// the user code by calling .
///
[StringSyntax(StringSyntaxAttribute.Uri)]
- public string? RedirectUri { get; private set; }
+ public string? RedirectUri { get; internal set; }
///
/// Gets or sets the security principal extracted
@@ -77,6 +77,12 @@ public static partial class OpenIddictServerEvents
///
public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
+ ///
+ /// Gets or sets the security principal extracted from the
+ /// request token, if applicable.
+ ///
+ public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
+
///
/// Populates the property with the specified redirect_uri.
///
@@ -219,4 +225,200 @@ public static partial class OpenIddictServerEvents
///
public string? ResponseMode { get; set; }
}
+
+
+ ///
+ /// 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.
+ ///
+ public sealed class ExtractPushedAuthorizationRequestContext : BaseValidatingContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ExtractPushedAuthorizationRequestContext(OpenIddictServerTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request or if it was extracted yet.
+ ///
+ public OpenIddictRequest? Request
+ {
+ get => Transaction.Request;
+ set => Transaction.Request = value;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ public sealed class ValidatePushedAuthorizationRequestContext : BaseValidatingContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ValidatePushedAuthorizationRequestContext(OpenIddictServerTransaction transaction)
+ : base(transaction)
+ // Infer the redirect_uri from the value specified by the client application.
+ => RedirectUri = Request?.RedirectUri;
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets the client_id specified by the client application.
+ ///
+ public string? ClientId => Request?.ClientId;
+
+ ///
+ /// 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 .
+ ///
+ [StringSyntax(StringSyntaxAttribute.Uri)]
+ public string? RedirectUri { get; private set; }
+
+ ///
+ /// Gets or sets the security principal extracted
+ /// from the identity token hint, if applicable.
+ ///
+ public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
+
+ ///
+ /// Populates the property with the specified redirect_uri.
+ ///
+ /// The redirect_uri to use when redirecting the user agent.
+ 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;
+ }
+ }
+
+ ///
+ /// Represents an event called for each validated pushed authorization request
+ /// to allow the user code to decide how the request should be handled.
+ ///
+ public sealed class HandlePushedAuthorizationRequestContext : BaseValidatingTicketContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public HandlePushedAuthorizationRequestContext(OpenIddictServerTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the security principal extracted
+ /// from the identity token hint, if applicable.
+ ///
+ public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
+
+ ///
+ /// Gets the additional parameters returned to the client application.
+ ///
+ public Dictionary Parameters { get; private set; }
+ = new(StringComparer.Ordinal);
+
+ ///
+ /// Allows OpenIddict to return a sign-in response using the specified principal.
+ ///
+ /// The claims principal.
+ public void SignIn(ClaimsPrincipal principal) => Principal = principal;
+
+ ///
+ /// Allows OpenIddict to return a sign-in response using the specified principal.
+ ///
+ /// The claims principal.
+ /// The additional parameters returned to the client application.
+ public void SignIn(ClaimsPrincipal principal, IDictionary parameters)
+ {
+ Principal = principal;
+ Parameters = new(parameters, StringComparer.Ordinal);
+ }
+ }
+
+ ///
+ /// Represents an event called before the pushed authorization response is returned to the caller.
+ ///
+ public sealed class ApplyPushedAuthorizationResponseContext : BaseRequestContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ApplyPushedAuthorizationResponseContext(OpenIddictServerTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request, or if it couldn't be extracted.
+ ///
+ public OpenIddictRequest? Request
+ {
+ get => Transaction.Request;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the response.
+ ///
+ public OpenIddictResponse Response
+ {
+ get => Transaction.Response!;
+ set => Transaction.Response = value;
+ }
+
+ ///
+ /// Gets the access code expected to
+ /// be returned to the client application.
+ /// Depending on the flow, it may be null.
+ ///
+ public string? AccessToken => Response?.AccessToken;
+
+ ///
+ /// Gets the authorization code expected to
+ /// be returned to the client application.
+ /// Depending on the flow, it may be null.
+ ///
+ public string? AuthorizationCode => Response?.Code;
+
+ ///
+ /// Gets the error code returned to the client application.
+ /// When the response indicates a successful response,
+ /// this property returns .
+ ///
+ public string? Error => Response?.Error;
+ }
}
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
index 7370a7c5..50ac2b82 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
@@ -116,6 +116,11 @@ public static partial class OpenIddictServerEvents
///
public Uri? IntrospectionEndpoint { get; set; }
+ ///
+ /// Gets or sets the pushed authorization endpoint URI.
+ ///
+ public Uri? PushedAuthorizationEndpoint { get; set; }
+
///
/// Gets or sets the revocation endpoint URI.
///
@@ -171,6 +176,12 @@ public static partial class OpenIddictServerEvents
///
public HashSet PromptValues { get; } = new(StringComparer.Ordinal);
+ ///
+ /// Gets a list of client authentication methods supported by the pushed
+ /// authorization endpoint provided by the authorization server.
+ ///
+ public HashSet PushedAuthorizationEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
+
///
/// 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.
///
public HashSet TokenEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
+
+ ///
+ /// Gets or sets a boolean indicating whether pushed authorization requests are required.
+ ///
+ public bool RequirePushedAuthorizationRequests { get; set; }
}
///
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs
index 9980bb0b..83ba0512 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs
+++ b/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.
///
[StringSyntax(StringSyntaxAttribute.Uri)]
- public string? PostLogoutRedirectUri { get; private set; }
+ public string? PostLogoutRedirectUri { get; internal set; }
///
/// Gets or sets the security principal extracted
@@ -75,6 +75,12 @@ public static partial class OpenIddictServerEvents
///
public ClaimsPrincipal? IdentityTokenHintPrincipal { get; set; }
+ ///
+ /// Gets or sets the security principal extracted from the
+ /// request token, if applicable.
+ ///
+ public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
+
///
/// Populates the property with the specified redirect_uri.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs
index 391e1b03..a3733b6c 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs
@@ -381,6 +381,15 @@ public static partial class OpenIddictServerEvents
///
public bool ExtractRefreshToken { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether a request token
+ /// should be extracted from the current context.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool ExtractRequestToken { get; set; }
+
///
/// 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
///
public bool RequireRefreshToken { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether a request token
+ /// must be resolved for the authentication to be considered valid.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool RequireRequestToken { get; set; }
+
///
/// 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
///
public bool ValidateRefreshToken { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether the request token
+ /// extracted from the current request should be validated.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool ValidateRequestToken { get; set; }
+
///
/// 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
///
public bool RejectRefreshToken { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether an invalid request token
+ /// will cause the authentication demand to be rejected or will be ignored.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool RejectRequestToken { get; set; }
+
///
/// 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
///
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
+ ///
+ /// Gets or sets the request token to validate, if applicable.
+ ///
+ public string? RequestToken { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the request token, if applicable.
+ ///
+ public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
+
///
/// Gets or sets the user code to validate, if applicable.
///
@@ -825,6 +871,15 @@ public static partial class OpenIddictServerEvents
///
public bool GenerateRefreshToken { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether a request token
+ /// should be generated (and optionally returned to the client).
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool GenerateRequestToken { get; set; }
+
///
/// 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
///
public bool IncludeRefreshToken { get; set; }
+ ///
+ /// Gets or sets a boolean indicating whether the generated request token
+ /// should be returned to the client application as part of the response.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool IncludeRequestToken { get; set; }
+
///
/// 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
///
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
+ ///
+ /// Gets or sets the generated request token, if applicable.
+ /// The request token will only be returned if
+ /// is set to .
+ ///
+ public string? RequestToken { get; set; }
+
+ ///
+ /// Gets or sets the principal containing the claims that will be used
+ /// to create the request token, if applicable.
+ ///
+ public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
+
///
/// Gets or sets the generated user code, if applicable.
/// The user code will only be returned if
diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
index 476a71d6..1e1c10c1 100644
--- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
@@ -67,10 +67,14 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
index a73d1971..392a689d 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
@@ -470,6 +470,23 @@ public static class OpenIddictServerHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if the request is not a pushed authorization request.
+ ///
+ public sealed class RequirePushedAuthorizationRequest : IOpenIddictServerHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(BaseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.EndpointType is OpenIddictServerEndpointType.PushedAuthorization);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if reference access tokens are disabled.
///
@@ -504,6 +521,57 @@ public static class OpenIddictServerHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no request token is generated.
+ ///
+ public sealed class RequireRequestTokenGenerated : IOpenIddictServerHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(ProcessSignInContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.GenerateRequestToken);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if no request token principal is available.
+ ///
+ public sealed class RequireRequestTokenPrincipal : IOpenIddictServerHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.RequestTokenPrincipal is not null);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if no request token is validated.
+ ///
+ public sealed class RequireRequestTokenValidated : IOpenIddictServerHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.ValidateRequestToken);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if no refresh token is generated.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
index 2dd60f63..790c109c 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
@@ -6,6 +6,9 @@
using System.Collections.Immutable;
using System.Diagnostics;
+using System.Globalization;
+using System.Security.Claims;
+using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -35,6 +38,8 @@ public static partial class OpenIddictServerHandlers
ValidateRequestParameter.Descriptor,
ValidateRequestUriParameter.Descriptor,
ValidateClientIdParameter.Descriptor,
+ ValidateAuthentication.Descriptor,
+ RestorePushedAuthorizationRequestParameters.Descriptor,
ValidateRedirectUriParameter.Descriptor,
ValidateResponseTypeParameter.Descriptor,
ValidateResponseModeParameter.Descriptor,
@@ -42,7 +47,6 @@ public static partial class OpenIddictServerHandlers
ValidateNonceParameter.Descriptor,
ValidatePromptParameter.Descriptor,
ValidateProofKeyForCodeExchangeParameters.Descriptor,
- ValidateAuthentication.Descriptor,
ValidateResponseType.Descriptor,
ValidateClientRedirectUri.Descriptor,
ValidateScopes.Descriptor,
@@ -50,6 +54,7 @@ public static partial class OpenIddictServerHandlers
ValidateGrantTypePermissions.Descriptor,
ValidateResponseTypePermissions.Descriptor,
ValidateScopePermissions.Descriptor,
+ ValidatePushedAuthorizationRequestsRequirement.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateAuthorizedParty.Descriptor,
@@ -64,7 +69,47 @@ public static partial class OpenIddictServerHandlers
AttachRedirectUri.Descriptor,
InferResponseMode.Descriptor,
AttachResponseState.Descriptor,
- AttachIssuer.Descriptor
+ AttachIssuer.Descriptor,
+
+ /*
+ * Pushed authorization request top-level processing:
+ */
+ ExtractPushedAuthorizationRequest.Descriptor,
+ ValidatePushedAuthorizationRequest.Descriptor,
+ HandlePushedAuthorizationRequest.Descriptor,
+ ApplyPushedAuthorizationResponse.Descriptor,
+ ApplyPushedAuthorizationResponse.Descriptor,
+ ApplyPushedAuthorizationResponse.Descriptor,
+ ApplyPushedAuthorizationResponse.Descriptor,
+
+ /*
+ * Pushed authorization request validation:
+ */
+ ValidatePushedRequestParameter.Descriptor,
+ ValidatePushedRequestUriParameter.Descriptor,
+ ValidatePushedClientIdParameter.Descriptor,
+ ValidatePushedRedirectUriParameter.Descriptor,
+ ValidatePushedResponseTypeParameter.Descriptor,
+ ValidatePushedResponseModeParameter.Descriptor,
+ ValidatePushedScopeParameter.Descriptor,
+ ValidatePushedNonceParameter.Descriptor,
+ ValidatePushedPromptParameter.Descriptor,
+ ValidatePushedProofKeyForCodeExchangeParameters.Descriptor,
+ ValidatePushedAuthentication.Descriptor,
+ ValidatePushedResponseType.Descriptor,
+ ValidatePushedClientRedirectUri.Descriptor,
+ ValidatePushedScopes.Descriptor,
+ ValidatePushedEndpointPermissions.Descriptor,
+ ValidatePushedGrantTypePermissions.Descriptor,
+ ValidatePushedResponseTypePermissions.Descriptor,
+ ValidatePushedScopePermissions.Descriptor,
+ ValidatePushedProofKeyForCodeExchangeRequirement.Descriptor,
+ ValidatePushedAuthorizedParty.Descriptor,
+
+ /*
+ * Pushed authorization request handling:
+ */
+ AttachPushedPrincipal.Descriptor
]);
///
@@ -288,6 +333,47 @@ public static partial class OpenIddictServerHandlers
}
}
+ else if (context.Options.EnableAuthorizationRequestCaching &&
+ 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.ID0029));
}
}
@@ -381,7 +467,7 @@ public static partial class OpenIddictServerHandlers
}
///
- /// Contains the logic responsible for rejecting authorization requests that specify the unsupported request_uri parameter.
+ /// Contains the logic responsible for rejecting authorization requests that specify an invalid request_uri parameter.
///
public sealed class ValidateRequestUriParameter : IOpenIddictServerHandler
{
@@ -403,8 +489,28 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
- // Reject requests using the unsupported request_uri parameter.
- if (!string.IsNullOrEmpty(context.Request.RequestUri))
+ if (string.IsNullOrEmpty(context.Request.RequestUri))
+ {
+ // If OpenIddict was configured to globally require pushed authorization requests,
+ // eagerly reject the request if the "request_uri" parameter is missing or empty.
+ if (context.Options.RequirePushedAuthorizationRequests)
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.RequestUri),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ return default;
+ }
+
+ // OpenIddict only supports "request_uri" parameters containing a reference to a request
+ // token generated during a pushed authorization response or via the automatic request
+ // caching feature when explicitly enabled in the options. 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.Logger.LogInformation(SR.GetResourceString(SR.ID6032), Parameters.RequestUri);
@@ -416,6 +522,20 @@ public static partial class OpenIddictServerHandlers
return default;
}
+ // Both the OpenID Connect core and OAuth 2.0 JWT-Secured Authorization Request specifications
+ // require attaching the client identifier as a regular OAuth 2.0 authorization request parameter.
+ //
+ // See https://datatracker.ietf.org/doc/html/rfc9101#section-5 for more information.
+ if (string.IsNullOrEmpty(context.Request.ClientId))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2177(Parameters.ClientId),
+ uri: SR.FormatID8000(SR.ID2177));
+
+ return default;
+ }
+
return default;
}
}
@@ -461,6 +581,148 @@ public static partial class OpenIddictServerHandlers
}
}
+ ///
+ /// Contains the logic responsible for applying the authentication logic to authorization requests.
+ ///
+ public sealed class ValidateAuthentication : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictServerDispatcher _dispatcher;
+
+ public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseScopedHandler()
+ .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = new ProcessAuthenticationContext(context.Transaction);
+ await _dispatcher.DispatchAsync(notification);
+
+ // Store the context object in the transaction so it can be later retrieved by handlers
+ // that want to access the authentication result without triggering a new authentication flow.
+ context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, 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;
+ }
+
+ // Attach the security principals extracted from the tokens to the validation context.
+ context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal;
+ context.RequestTokenPrincipal = notification.RequestTokenPrincipal;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for restoring the parameters attached to the pushed authorization request.
+ ///
+ public sealed class RestorePushedAuthorizationRequestParameters : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateAuthorizationRequestContext 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
+ };
+
+ // Ensure the client_id attached to the regular authorization request
+ // matches the value present in the request token principal.
+ if (!string.Equals(request.ClientId, context.Request.ClientId, StringComparison.Ordinal))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2178(Parameters.ClientId),
+ uri: SR.FormatID8000(SR.ID2178));
+
+ return default;
+ }
+
+ // Note: the "request" and "request_uri" parameters have been initially introduced by the OpenID Connect
+ // core specification, that allows overriding the parameters contained in the request object by attaching
+ // parameters to the query string (or to the request form, for POST requests) of the authorization request.
+ // This mechanism allows using pre-computed or static request objects while still being able to attach
+ // dynamic values (e.g a state value) to the authorization requests. Unfortunately, when this feature was
+ // backported to OAuth 2.0 by the OAuth 2.0 JWT-Secured Authorization Request specification, an incompatible
+ // design was defined, as authorization servers MUST now ignore parameters that are attached as regular
+ // OAuth 2.0 parameters to the authorization requests (i.e not attached to the request object/PAR request).
+ //
+ // Since the design defined in the OAuth 2.0 JWT-Secured Authorization Request specification is safer, it
+ // is the approach implemented by OpenIddict, that ignores all the parameters directly attached to the
+ // authorization requests when a request token (e.g retrieved using a pushed authorization request) is used.
+ //
+ // For more information, see https://datatracker.ietf.org/doc/html/rfc9101#section-5 and
+ // https://openid.net/specs/openid-connect-core-1_0.html#RequestUriRationale.
+
+ // Note: the prompt parameter is special-cased to allow application code to override the "login" prompt
+ // value after redirecting the user agent to the login endpoint and asking the user to re-authenticate.
+ if (request.HasPromptValue(PromptValues.Login) && context.Request.HasParameter(Parameters.Prompt) &&
+ !context.Request.HasPromptValue(PromptValues.Login))
+ {
+ request.Prompt = string.Join(",", request.GetPromptValues()
+ .ToImmutableHashSet(StringComparer.Ordinal)
+ .Remove(PromptValues.Login));
+ }
+
+ context.Request = request;
+ context.RedirectUri = request.RedirectUri;
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for rejecting authorization requests that lack the mandatory redirect_uri parameter.
///
@@ -472,7 +734,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000)
+ .SetOrder(RestorePushedAuthorizationRequestParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -1023,67 +1285,6 @@ public static partial class OpenIddictServerHandlers
}
}
- ///
- /// Contains the logic responsible for applying the authentication logic to authorization requests.
- ///
- public sealed class ValidateAuthentication : IOpenIddictServerHandler
- {
- private readonly IOpenIddictServerDispatcher _dispatcher;
-
- public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher)
- => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .UseScopedHandler()
- .SetOrder(ValidateProofKeyForCodeExchangeParameters.Descriptor.Order + 1_000)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- var notification = new ProcessAuthenticationContext(context.Transaction);
- await _dispatcher.DispatchAsync(notification);
-
- // Store the context object in the transaction so it can be later retrieved by handlers
- // that want to access the authentication result without triggering a new authentication flow.
- context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, 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;
- }
-
- // Attach the security principal extracted from the token to the validation context.
- context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal;
- }
- }
-
///
/// Contains the logic responsible for rejecting authorization requests that use an unsafe response type.
///
@@ -1110,7 +1311,7 @@ public static partial class OpenIddictServerHandlers
new ValidateResponseType(provider.GetService() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
- .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000)
+ .SetOrder(ValidateProofKeyForCodeExchangeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -1627,16 +1828,16 @@ public static partial class OpenIddictServerHandlers
///
/// Contains the logic responsible for rejecting authorization requests made by
- /// applications for which proof key for code exchange (PKCE) was enforced.
+ /// applications for which pushed authorization requests (PAR) are enforced.
/// Note: this handler is not used when the degraded mode is enabled.
///
- public sealed class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler
+ public sealed class ValidatePushedAuthorizationRequestsRequirement : IOpenIddictServerHandler
{
private readonly IOpenIddictApplicationManager _applicationManager;
- public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+ public ValidatePushedAuthorizationRequestsRequirement() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
- public ValidateProofKeyForCodeExchangeRequirement(IOpenIddictApplicationManager applicationManager)
+ public ValidatePushedAuthorizationRequestsRequirement(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
///
@@ -1645,7 +1846,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder()
.AddFilter()
- .UseScopedHandler()
+ .UseScopedHandler()
.SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -1660,9 +1861,10 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
- // If a code_challenge was provided or if no authorization code is requested, the request is always
- // considered valid, whether the proof key for code exchange requirement is enforced or not.
- if (!string.IsNullOrEmpty(context.Request.CodeChallenge) || !context.Request.HasResponseType(ResponseTypes.Code))
+ // If a request token principal with the correct type could be extracted, the request is always
+ // considered valid, whether the pushed authorization requests requirement is enforced or not.
+ var type = context.RequestTokenPrincipal?.GetClaim(Claims.Private.RequestTokenType);
+ if (type is RequestTokenTypes.Private.PushedAuthorizationRequest)
{
return;
}
@@ -1670,14 +1872,24 @@ public static partial class OpenIddictServerHandlers
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
- if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
+ if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.PushedAuthorizationRequests))
{
- context.Logger.LogInformation(SR.GetResourceString(SR.ID6033), Parameters.CodeChallenge);
+ if (string.IsNullOrEmpty(context.Request.RequestUri))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6033), Parameters.RequestUri);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2054(Parameters.RequestUri),
+ uri: SR.FormatID8000(SR.ID2054));
+
+ return;
+ }
context.Reject(
error: Errors.InvalidRequest,
- description: SR.FormatID2054(Parameters.CodeChallenge),
- uri: SR.FormatID8000(SR.ID2054));
+ description: SR.FormatID2182(Parameters.RequestUri),
+ uri: SR.FormatID8000(SR.ID2182));
return;
}
@@ -1685,8 +1897,67 @@ public static partial class OpenIddictServerHandlers
}
///
- /// Contains the logic responsible for rejecting authorization requests that specify an identity
- /// token hint that cannot be used by the client application sending the authorization request.
+ /// Contains the logic responsible for rejecting authorization requests made by
+ /// applications for which proof key for code exchange (PKCE) was enforced.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public sealed class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidateProofKeyForCodeExchangeRequirement(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedAuthorizationRequestsRequirement.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ // If a code_challenge was provided or if no authorization code is requested, the request is always
+ // considered valid, whether the proof key for code exchange requirement is enforced or not.
+ if (!string.IsNullOrEmpty(context.Request.CodeChallenge) || !context.Request.HasResponseType(ResponseTypes.Code))
+ {
+ return;
+ }
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6033), Parameters.CodeChallenge);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2054(Parameters.CodeChallenge),
+ uri: SR.FormatID8000(SR.ID2054));
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting authorization requests that specify an identity
+ /// token hint that cannot be used by the client application sending the authorization request.
///
public sealed class ValidateAuthorizedParty : IOpenIddictServerHandler
{
@@ -1792,7 +2063,9 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
- if (context.Request is null)
+ // If the authorization response contains a request token, do not use the
+ // 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;
}
@@ -1843,7 +2116,7 @@ public static partial class OpenIddictServerHandlers
context.ResponseMode = context.Request.ResponseMode;
// If the response_mode parameter was not specified, try to infer it.
- if (string.IsNullOrEmpty(context.ResponseMode) && !string.IsNullOrEmpty(context.RedirectUri))
+ if (!string.IsNullOrEmpty(context.RedirectUri) && string.IsNullOrEmpty(context.ResponseMode))
{
context.ResponseMode = context.Request.IsFormPostResponseMode() ? ResponseModes.FormPost :
context.Request.IsFragmentResponseMode() ? ResponseModes.Fragment :
@@ -1935,5 +2208,1722 @@ public static partial class OpenIddictServerHandlers
return default;
}
}
+
+ ///
+ /// Contains the logic responsible for extracting pushed authorization
+ /// requests and invoking the corresponding event handlers.
+ ///
+ public sealed class ExtractPushedAuthorizationRequest : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictServerDispatcher _dispatcher;
+
+ public ExtractPushedAuthorizationRequest(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(100_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = new ExtractPushedAuthorizationRequestContext(context.Transaction);
+ 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;
+ }
+
+ if (notification.Request is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0468));
+ }
+
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6237), notification.Request);
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for validating pushed authorization
+ /// requests and invoking the corresponding event handlers.
+ ///
+ public sealed class ValidatePushedAuthorizationRequest : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictServerDispatcher _dispatcher;
+
+ public ValidatePushedAuthorizationRequest(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ExtractPushedAuthorizationRequest.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = new ValidatePushedAuthorizationRequestContext(context.Transaction);
+ await _dispatcher.DispatchAsync(notification);
+
+ // Store the context object in the transaction so it can be later retrieved by handlers
+ // that want to access the redirect_uri without triggering a new validation process.
+ context.Transaction.SetProperty(typeof(ValidatePushedAuthorizationRequestContext).FullName!, 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;
+ }
+
+ if (string.IsNullOrEmpty(notification.RedirectUri))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0028));
+ }
+
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6238));
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for handling pushed authorization
+ /// requests and invoking the corresponding event handlers.
+ ///
+ public sealed class HandlePushedAuthorizationRequest : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictServerDispatcher _dispatcher;
+
+ public HandlePushedAuthorizationRequest(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedAuthorizationRequest.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = new HandlePushedAuthorizationRequestContext(context.Transaction);
+ 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;
+ }
+
+ if (notification.Principal is null)
+ {
+ // Note: no authentication type is deliberately specified to represent an unauthenticated identity.
+ var principal = new ClaimsPrincipal(new ClaimsIdentity());
+ principal.SetScopes(notification.Request.GetScopes());
+
+ notification.Principal = principal;
+ }
+
+ var @event = new ProcessSignInContext(context.Transaction)
+ {
+ Principal = notification.Principal,
+ 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: @event.Error ?? Errors.InvalidGrant,
+ description: @event.ErrorDescription,
+ uri: @event.ErrorUri);
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for processing pushed authorization
+ /// responses and invoking the corresponding event handlers.
+ ///
+ public sealed class ApplyPushedAuthorizationResponse : IOpenIddictServerHandler where TContext : BaseRequestContext
+ {
+ private readonly IOpenIddictServerDispatcher _dispatcher;
+
+ public ApplyPushedAuthorizationResponse(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler>()
+ .SetOrder(500_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(TContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = new ApplyPushedAuthorizationResponseContext(context.Transaction);
+ await _dispatcher.DispatchAsync(notification);
+
+ if (notification.IsRequestHandled)
+ {
+ context.HandleRequest();
+ return;
+ }
+
+ else if (notification.IsRequestSkipped)
+ {
+ context.SkipRequest();
+ return;
+ }
+
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0469));
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that specify the unsupported request parameter.
+ ///
+ public sealed class ValidatePushedRequestParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject requests using the unsupported request parameter.
+ if (!string.IsNullOrEmpty(context.Request.Request))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6239), Parameters.Request);
+
+ context.Reject(
+ error: Errors.RequestNotSupported,
+ description: SR.FormatID2028(Parameters.Request),
+ uri: SR.FormatID8000(SR.ID2028));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that specify the forbidden request_uri parameter.
+ ///
+ public sealed class ValidatePushedRequestUriParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedRequestParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject requests using the request_uri parameter, as this parameter is explicitly forbidden
+ // by the OAuth 2.0 Pushed Authorization Requests specification when used in PAR requests.
+ //
+ // See https://datatracker.ietf.org/doc/html/rfc9126#section-2.1 for more information.
+ if (!string.IsNullOrEmpty(context.Request.RequestUri))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2074(Parameters.RequestUri),
+ uri: SR.FormatID8000(SR.ID2074));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that lack the mandatory client_id parameter.
+ ///
+ public sealed class ValidatePushedClientIdParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedRequestUriParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // client_id is a required parameter and MUST cause an error when missing.
+ // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.
+ if (string.IsNullOrEmpty(context.ClientId))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.ClientId);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.ClientId),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that lack the mandatory redirect_uri parameter.
+ ///
+ public sealed class ValidatePushedRedirectUriParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedClientIdParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // While redirect_uri was not mandatory in OAuth 2.0, this parameter
+ // is now declared as REQUIRED and MUST cause an error when missing.
+ // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.
+ // To keep OpenIddict compatible with pure OAuth 2.0 clients, an error
+ // is only returned if the request was made by an OpenID Connect client.
+ if (string.IsNullOrEmpty(context.RedirectUri))
+ {
+ if (context.Request.HasScope(Scopes.OpenId))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.RedirectUri);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.RedirectUri),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ return default;
+ }
+
+ // Note: when specified, redirect_uri MUST be an absolute URI.
+ // See http://tools.ietf.org/html/rfc6749#section-3.1.2
+ // and http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.
+ if (!Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6241), Parameters.RedirectUri, context.RedirectUri);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2030(Parameters.RedirectUri),
+ uri: SR.FormatID8000(SR.ID2030));
+
+ return default;
+ }
+
+ // Note: when specified, redirect_uri MUST NOT include a fragment component.
+ // See http://tools.ietf.org/html/rfc6749#section-3.1.2
+ // and http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+ if (!string.IsNullOrEmpty(uri.Fragment))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6242), Parameters.RedirectUri, context.RedirectUri);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2031(Parameters.RedirectUri),
+ uri: SR.FormatID8000(SR.ID2031));
+
+ return default;
+ }
+
+ // To prevent issuer fixation attacks where a malicious client would specify an "iss" parameter
+ // in the redirect_uri, ensure the query - if present - doesn't include an "iss" parameter.
+ //
+ // Note: while OAuth 2.0 parameters are case-sentitive, the following check deliberately
+ // uses a case-insensitive comparison to ensure that all variations of "iss" are rejected.
+ if (!string.IsNullOrEmpty(uri.Query))
+ {
+ var parameters = OpenIddictHelpers.ParseQuery(uri.Query);
+ if (parameters.ContainsKey(Parameters.Iss))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6259), Parameters.RedirectUri, Parameters.Iss);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2135(Parameters.RedirectUri, Parameters.Iss),
+ uri: SR.FormatID8000(SR.ID2135));
+
+ return default;
+ }
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that specify an invalid response_type parameter.
+ ///
+ public sealed class ValidatePushedResponseTypeParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedRedirectUriParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject requests missing the mandatory response_type parameter.
+ if (string.IsNullOrEmpty(context.Request.ResponseType))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.ResponseType);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ // Reject code flow requests if the server is not configured to allow the authorization code grant type.
+ if (context.Request.IsAuthorizationCodeFlow() && !context.Options.GrantTypes.Contains(GrantTypes.AuthorizationCode))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6243), context.Request.ResponseType);
+
+ context.Reject(
+ error: Errors.UnsupportedResponseType,
+ description: SR.FormatID2032(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+
+ // Reject implicit flow requests if the server is not configured to allow the implicit grant type.
+ if (context.Request.IsImplicitFlow() && !context.Options.GrantTypes.Contains(GrantTypes.Implicit))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6243), context.Request.ResponseType);
+
+ context.Reject(
+ error: Errors.UnsupportedResponseType,
+ description: SR.FormatID2032(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+
+ // Reject hybrid flow requests if the server is not configured to allow the authorization code or implicit grant types.
+ if (context.Request.IsHybridFlow() && (!context.Options.GrantTypes.Contains(GrantTypes.AuthorizationCode) ||
+ !context.Options.GrantTypes.Contains(GrantTypes.Implicit)))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6243), context.Request.ResponseType);
+
+ context.Reject(
+ error: Errors.UnsupportedResponseType,
+ description: SR.FormatID2032(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+
+ // Prevent response_type=none from being used with any other value.
+ // See https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none for more information.
+ var types = context.Request.GetResponseTypes().ToHashSet(StringComparer.Ordinal);
+ if (types.Count > 1 && types.Contains(ResponseTypes.None))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6260), context.Request.ResponseType);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2052(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2052));
+
+ return default;
+ }
+
+ // Reject requests that specify an unsupported response_type.
+ if (!context.Options.ResponseTypes.Any(type => types.SetEquals(
+ type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6243), context.Request.ResponseType);
+
+ context.Reject(
+ error: Errors.UnsupportedResponseType,
+ description: SR.FormatID2032(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that specify an invalid response_mode parameter.
+ ///
+ public sealed class ValidatePushedResponseModeParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedResponseTypeParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // response_mode=query (explicit or not) and a response_type containing id_token
+ // or token are not considered as a safe combination and MUST be rejected.
+ // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security.
+ if (context.Request.IsQueryResponseMode() && (context.Request.HasResponseType(ResponseTypes.IdToken) ||
+ context.Request.HasResponseType(ResponseTypes.Token)))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6244), context.Request.ResponseType, context.Request.ResponseMode);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2033(Parameters.ResponseType, Parameters.ResponseMode),
+ uri: SR.FormatID8000(SR.ID2033));
+
+ return default;
+ }
+
+ // Reject requests that specify an unsupported response_mode or don't specify a different response_mode
+ // if the default response_mode inferred from the response_type was explicitly disabled in the options.
+ if (!ValidatePushedResponseMode(context.Request, context.Options))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6245), context.Request.ResponseMode);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2032(Parameters.ResponseMode),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+
+ return default;
+
+ static bool ValidatePushedResponseMode(OpenIddictRequest request, OpenIddictServerOptions options)
+ {
+ // Note: both the fragment and query response modes are used as default response modes
+ // when using the implicit/hybrid and code flows if no explicit value was set.
+ // To ensure requests are rejected if the default response mode was manually disabled,
+ // the fragment and query response modes are checked first using the appropriate extensions.
+
+ if (request.IsFragmentResponseMode())
+ {
+ return options.ResponseModes.Contains(ResponseModes.Fragment);
+ }
+
+ if (request.IsQueryResponseMode())
+ {
+ return options.ResponseModes.Contains(ResponseModes.Query);
+ }
+
+ if (string.IsNullOrEmpty(request.ResponseMode))
+ {
+ return true;
+ }
+
+ return options.ResponseModes.Contains(request.ResponseMode);
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization
+ /// requests that don't specify a valid scope parameter.
+ ///
+ public sealed class ValidatePushedScopeParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedResponseModeParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject pushed authorization requests containing the id_token response_type if no openid scope has been received.
+ if (context.Request.HasResponseType(ResponseTypes.IdToken) && !context.Request.HasScope(Scopes.OpenId))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6246), Scopes.OpenId);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2034(Scopes.OpenId),
+ uri: SR.FormatID8000(SR.ID2034));
+
+ return default;
+ }
+
+ // Reject pushed authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
+ if (context.Request.HasScope(Scopes.OfflineAccess) && !context.Options.GrantTypes.Contains(GrantTypes.RefreshToken))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2035(Scopes.OfflineAccess),
+ uri: SR.FormatID8000(SR.ID2035));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that don't specify a nonce.
+ ///
+ public sealed class ValidatePushedNonceParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedScopeParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject OpenID Connect implicit/hybrid requests missing the mandatory nonce parameter.
+ // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest,
+ // http://openid.net/specs/openid-connect-implicit-1_0.html#RequestParameters
+ // and http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken.
+
+ if (!string.IsNullOrEmpty(context.Request.Nonce) || !context.Request.HasScope(Scopes.OpenId))
+ {
+ return default;
+ }
+
+ if (context.Request.IsImplicitFlow() || context.Request.IsHybridFlow())
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.Nonce);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.Nonce),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that don't specify a valid prompt parameter.
+ ///
+ public sealed class ValidatePushedPromptParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedNonceParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (string.IsNullOrEmpty(context.Request.Prompt))
+ {
+ return default;
+ }
+
+ // Reject requests specifying an unsupported prompt value.
+ // See https://openid.net/specs/openid-connect-prompt-create-1_0.html#section-4.1 for more information.
+ foreach (var value in context.Request.GetPromptValues().ToHashSet(StringComparer.Ordinal))
+ {
+ if (!context.Options.PromptValues.Contains(value))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6261));
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2032(Parameters.Prompt),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+ }
+
+ // Reject requests specifying prompt=none with consent/login or select_account.
+ // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
+ if (context.Request.HasPromptValue(PromptValues.None) && (context.Request.HasPromptValue(PromptValues.Consent) ||
+ context.Request.HasPromptValue(PromptValues.Login) ||
+ context.Request.HasPromptValue(PromptValues.SelectAccount)))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6247));
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2052(Parameters.Prompt),
+ uri: SR.FormatID8000(SR.ID2052));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that don't specify valid PKCE parameters.
+ ///
+ public sealed class ValidatePushedProofKeyForCodeExchangeParameters : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedPromptParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If OpenIddict was configured to require PKCE, reject the request if the code challenge
+ // is missing and if an authorization code was requested by the client application.
+ if (context.Options.RequireProofKeyForCodeExchange &&
+ context.Request.HasResponseType(ResponseTypes.Code) &&
+ string.IsNullOrEmpty(context.Request.CodeChallenge))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.CodeChallenge);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.CodeChallenge),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ // At this point, stop validating the PKCE parameters if both the
+ // code_challenge and code_challenge_method parameter are missing.
+ if (string.IsNullOrEmpty(context.Request.CodeChallenge) &&
+ string.IsNullOrEmpty(context.Request.CodeChallengeMethod))
+ {
+ return default;
+ }
+
+ // Ensure a code_challenge was specified if a code_challenge_method was used.
+ if (string.IsNullOrEmpty(context.Request.CodeChallenge))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.CodeChallenge);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2037(Parameters.CodeChallengeMethod, Parameters.CodeChallenge),
+ uri: SR.FormatID8000(SR.ID2037));
+
+ return default;
+ }
+
+ // If the plain code challenge method was not explicitly enabled,
+ // reject the request indicating that a method must be set.
+ if (string.IsNullOrEmpty(context.Request.CodeChallengeMethod) &&
+ !context.Options.CodeChallengeMethods.Contains(CodeChallengeMethods.Plain))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.CodeChallengeMethod);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.CodeChallengeMethod),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return default;
+ }
+
+ // If a code_challenge_method was specified, ensure the algorithm is supported.
+ if (!string.IsNullOrEmpty(context.Request.CodeChallengeMethod) &&
+ !context.Options.CodeChallengeMethods.Contains(context.Request.CodeChallengeMethod))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6248));
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2032(Parameters.CodeChallengeMethod),
+ uri: SR.FormatID8000(SR.ID2032));
+
+ return default;
+ }
+
+ // When code_challenge or code_challenge_method is specified, ensure the response_type includes "code".
+ if (!context.Request.HasResponseType(ResponseTypes.Code))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6249));
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2040(Parameters.CodeChallenge, Parameters.CodeChallengeMethod, ResponseTypes.Code),
+ uri: SR.FormatID8000(SR.ID2040));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for applying the authentication logic to pushed authorization requests.
+ ///
+ public sealed class ValidatePushedAuthentication : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictServerDispatcher _dispatcher;
+
+ public ValidatePushedAuthentication(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedProofKeyForCodeExchangeParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = new ProcessAuthenticationContext(context.Transaction);
+ await _dispatcher.DispatchAsync(notification);
+
+ // Store the context object in the transaction so it can be later retrieved by handlers
+ // that want to access the authentication result without triggering a new authentication flow.
+ context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, 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;
+ }
+
+ // Attach the security principal extracted from the token to the validation context.
+ context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that use an unsafe response type.
+ ///
+ public sealed class ValidatePushedResponseType : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager? _applicationManager;
+
+ public ValidatePushedResponseType(IOpenIddictApplicationManager? applicationManager = null)
+ => _applicationManager = applicationManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseScopedHandler(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>().CurrentValue;
+
+ return options.EnableDegradedMode ?
+ new ValidatePushedResponseType(applicationManager: null) :
+ new ValidatePushedResponseType(provider.GetService() ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
+ })
+ .SetOrder(ValidatePushedAuthentication.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Note: this handler is responsible for enforcing additional response_type requirements when
+ // response type permissions are not used (and thus cannot be finely controlled per client).
+ //
+ // Users who want to support the scenarios disallowed by this event handler are encouraged
+ // to re-enable permissions validation. Alternatively, this handler can be removed from
+ // the handlers list and replaced by a custom version using the events model APIs.
+ if (!context.Options.IgnoreResponseTypePermissions)
+ {
+ return;
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ // When PKCE is used, reject pushed authorization requests returning an access token directly
+ // from the authorization endpoint to prevent a malicious client from retrieving a valid
+ // access token - even with a limited scope - without sending the correct code_verifier.
+ if (!string.IsNullOrEmpty(context.Request.CodeChallenge) &&
+ context.Request.HasResponseType(ResponseTypes.Token))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6250));
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.FormatID2041(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2041));
+
+ return;
+ }
+
+ if (!context.Options.EnableDegradedMode)
+ {
+ if (_applicationManager is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+ }
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ // To prevent downgrade attacks, ensure that pushed authorization requests returning
+ // an access token directly from the authorization endpoint are rejected if
+ // the client_id corresponds to a confidential application.
+ if (context.Request.HasResponseType(ResponseTypes.Token) &&
+ await _applicationManager.HasClientTypeAsync(application, ClientTypes.Confidential))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6251), context.ClientId);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.FormatID2043(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2043));
+
+ return;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that use an invalid redirect_uri.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public sealed class ValidatePushedClientRedirectUri : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidatePushedClientRedirectUri() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidatePushedClientRedirectUri(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedResponseType.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ // If no explicit redirect_uri was specified, retrieve the URI associated with the
+ // client and ensure exactly one redirect_uri was attached to the client definition.
+ if (string.IsNullOrEmpty(context.RedirectUri))
+ {
+ var uris = await _applicationManager.GetRedirectUrisAsync(application);
+ if (uris.Length is not 1)
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.RedirectUri);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2029(Parameters.RedirectUri),
+ uri: SR.FormatID8000(SR.ID2029));
+
+ return;
+ }
+
+ context.SetRedirectUri(uris[0]);
+
+ return;
+ }
+
+ // Otherwise, ensure that the specified redirect_uri is valid and is associated with the client application.
+ if (!await _applicationManager.ValidateRedirectUriAsync(application, context.RedirectUri))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6252), context.RedirectUri);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2043(Parameters.RedirectUri),
+ uri: SR.FormatID8000(SR.ID2043));
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that use unregistered scopes.
+ /// Note: this handler partially works with the degraded mode but is not used when scope validation is disabled.
+ ///
+ public sealed class ValidatePushedScopes : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictScopeManager? _scopeManager;
+
+ public ValidatePushedScopes(IOpenIddictScopeManager? scopeManager = null)
+ => _scopeManager = scopeManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler(static provider =>
+ {
+ // Note: the scope 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>().CurrentValue;
+
+ return options.EnableDegradedMode ?
+ new ValidatePushedScopes() :
+ new ValidatePushedScopes(provider.GetService() ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
+ })
+ .SetOrder(ValidatePushedClientRedirectUri.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If all the specified scopes are registered in the options, avoid making a database lookup.
+ var scopes = context.Request.GetScopes().ToHashSet(StringComparer.Ordinal);
+ scopes.ExceptWith(context.Options.Scopes);
+
+ // Note: the remaining scopes are only checked if the degraded mode was not enabled,
+ // as this requires using the scope manager, which is never used with the degraded mode,
+ // even if the service was registered and resolved from the dependency injection container.
+ if (scopes.Count is not 0 && !context.Options.EnableDegradedMode)
+ {
+ if (_scopeManager is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+ }
+
+ await foreach (var scope in _scopeManager.FindByNamesAsync(scopes.ToImmutableArray()))
+ {
+ var name = await _scopeManager.GetNameAsync(scope);
+ if (!string.IsNullOrEmpty(name))
+ {
+ scopes.Remove(name);
+ }
+ }
+ }
+
+ // If at least one scope was not recognized, return an error.
+ if (scopes.Count is not 0)
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6253), scopes);
+
+ context.Reject(
+ error: Errors.InvalidScope,
+ description: SR.FormatID2052(Parameters.Scope),
+ uri: SR.FormatID8000(SR.ID2052));
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests made by unauthorized applications.
+ /// Note: this handler is not used when the degraded mode is enabled or when endpoint permissions are disabled.
+ ///
+ public sealed class ValidatePushedEndpointPermissions : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidatePushedEndpointPermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidatePushedEndpointPermissions(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedScopes.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ // Reject the request if the application is not allowed to use the pushed authorization endpoint.
+ if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.PushedAuthorization))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6254), context.ClientId);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.GetResourceString(SR.ID2183),
+ uri: SR.FormatID8000(SR.ID2183));
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests made by unauthorized applications.
+ /// Note: this handler is not used when the degraded mode is enabled or when grant type permissions are disabled.
+ ///
+ public sealed class ValidatePushedGrantTypePermissions : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidatePushedGrantTypePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidatePushedGrantTypePermissions(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedEndpointPermissions.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ // Reject the request if the application is not allowed to use the authorization code grant.
+ if (context.Request.IsAuthorizationCodeFlow() &&
+ !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6255), context.ClientId);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.GetResourceString(SR.ID2047),
+ uri: SR.FormatID8000(SR.ID2047));
+
+ return;
+ }
+
+ // Reject the request if the application is not allowed to use the implicit grant.
+ if (context.Request.IsImplicitFlow() &&
+ !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.Implicit))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6256), context.ClientId);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.GetResourceString(SR.ID2048),
+ uri: SR.FormatID8000(SR.ID2048));
+
+ return;
+ }
+
+ // Reject the request if the application is not allowed to use the authorization code/implicit grants.
+ if (context.Request.IsHybridFlow() &&
+ (!await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode) ||
+ !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.Implicit)))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6257), context.ClientId);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.GetResourceString(SR.ID2049),
+ uri: SR.FormatID8000(SR.ID2049));
+
+ return;
+ }
+
+ // Reject the request if the offline_access scope was request and
+ // if the application is not allowed to use the refresh token grant.
+ if (context.Request.HasScope(Scopes.OfflineAccess) &&
+ !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6258), context.ClientId, Scopes.OfflineAccess);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2065(Scopes.OfflineAccess),
+ uri: SR.FormatID8000(SR.ID2065));
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests made by unauthorized applications.
+ /// Note: this handler is not used when the degraded mode is enabled or when grant type permissions are disabled.
+ ///
+ public sealed class ValidatePushedResponseTypePermissions : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidatePushedResponseTypePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidatePushedResponseTypePermissions(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedGrantTypePermissions.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ // Reject requests that specify a response_type for which no permission was granted.
+ if (!await HasPermissionAsync(context.Request.GetResponseTypes()))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6262), context.ClientId, context.Request.ResponseType);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.FormatID2043(Parameters.ResponseType),
+ uri: SR.FormatID8000(SR.ID2043));
+
+ return;
+ }
+
+ async ValueTask HasPermissionAsync(IEnumerable types)
+ {
+ // Note: response type permissions are always prefixed with "rst:".
+ const string prefix = Permissions.Prefixes.ResponseType;
+
+ foreach (var permission in await _applicationManager.GetPermissionsAsync(application))
+ {
+ // Ignore permissions that are not response type permissions.
+ if (!permission.StartsWith(prefix, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ // Note: response types can be specified in any order. To ensure permissions are correctly
+ // checked even if the order differs from the one specified in the request, a HashSet is used.
+ var values = permission[prefix.Length..].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries);
+ if (values.Length is not 0 && values.ToHashSet(StringComparer.Ordinal).SetEquals(types))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests made by unauthorized applications.
+ /// Note: this handler is not used when the degraded mode is enabled or when scope permissions are disabled.
+ ///
+ public sealed class ValidatePushedScopePermissions : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidatePushedScopePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidatePushedScopePermissions(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedResponseTypePermissions.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ foreach (var scope in context.Request.GetScopes())
+ {
+ // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes.
+ if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) ||
+ string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ // Reject the request if the application is not allowed to use the iterated scope.
+ if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6258), context.ClientId, scope);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.GetResourceString(SR.ID2051),
+ uri: SR.FormatID8000(SR.ID2051));
+
+ return;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests made by
+ /// applications for which proof key for code exchange (PKCE) was enforced.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public sealed class ValidatePushedProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidatePushedProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidatePushedProofKeyForCodeExchangeRequirement(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePushedScopePermissions.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ // If a code_challenge was provided or if no authorization code is requested, the request is always
+ // considered valid, whether the proof key for code exchange requirement is enforced or not.
+ if (!string.IsNullOrEmpty(context.Request.CodeChallenge) || !context.Request.HasResponseType(ResponseTypes.Code))
+ {
+ return;
+ }
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6240), Parameters.CodeChallenge);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2054(Parameters.CodeChallenge),
+ uri: SR.FormatID8000(SR.ID2054));
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for rejecting pushed authorization requests that specify an identity
+ /// token hint that cannot be used by the client application sending the pushed authorization request.
+ ///
+ public sealed class ValidatePushedAuthorizedParty : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidatePushedProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.IdentityTokenHintPrincipal is null)
+ {
+ return default;
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ // When an identity token hint is specified, the client_id (when present) must be
+ // listed either as a valid audience or as a presenter to be considered valid.
+ if (!context.IdentityTokenHintPrincipal.HasAudience(context.ClientId) &&
+ !context.IdentityTokenHintPrincipal.HasPresenter(context.ClientId))
+ {
+ context.Logger.LogWarning(SR.GetResourceString(SR.ID6263));
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.GetResourceString(SR.ID2141),
+ uri: SR.FormatID8000(SR.ID2141));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for attaching the principal
+ /// extracted from the identity token hint to the event context.
+ ///
+ public sealed class AttachPushedPrincipal : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandlePushedAuthorizationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var notification = context.Transaction.GetProperty(
+ typeof(ValidatePushedAuthorizationRequestContext).FullName!) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
+
+ context.IdentityTokenHintPrincipal ??= notification.IdentityTokenHintPrincipal;
+
+ return default;
+ }
+ }
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
index c6cea248..8f0b8b4b 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
+++ b/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
}
}
+ ///
+ /// Contains the logic responsible for attaching the security requirements to the provider discovery document.
+ ///
+ public sealed class AttachSecurityRequirements : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(AttachSigningAlgorithms.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleConfigurationRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.RequirePushedAuthorizationRequests = context.Options.RequirePushedAuthorizationRequests;
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for attaching additional metadata to the provider discovery document.
///
@@ -781,7 +824,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(AttachSigningAlgorithms.Descriptor.Order + 1_000)
+ .SetOrder(AttachSecurityRequirements.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
index c28299c0..eda2e55c 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
+++ b/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(type =>type switch
+ _ => context.ValidTokenTypes.SelectMany(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))
}
};
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
index ca498b94..2d203228 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
+++ b/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.Descriptor,
ApplyEndSessionResponse.Descriptor,
+ ApplyEndSessionResponse.Descriptor,
ApplyEndSessionResponse.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
}
}
+ ///
+ /// Contains the logic responsible for rejecting authorization requests that specify an invalid request_uri parameter.
+ ///
+ public sealed class ValidateRequestUriParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler