-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Support for Pushed Authorization Requests in OidcHandler #51686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
That implies |
I like this alternative, because it keeps the configuration simpler for the consumer of the API - it avoids the complexity of nullable booleans/ternary logic, and if you're working with a provider that declares its requirement for PAR in the metadata document, the handler just does the right thing. |
As part of the infrastructure we'll want some additional fields for this on OpenIdConnectMessage to indicate Pushed auth is being used (or a variant of CreateAuthenticationRequestUrl), as well as something like BuildFormPost for generating the push request. aspnetcore/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs Lines 483 to 496 in 73e0b53
|
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
|
What would be a good next step for this? Do we need more review/discussion of the API? Would it be helpful if I started a prototype/draft PR? |
@Tratcher I really appreciated your previous feedback on this. Can you give some guidance on what next steps should be please? |
You'll need to work with @halter73. |
I went ahead and did a bit of a proof-of-concept implementation, which I'll share in a moment as a draft PR. The things I've come across so far:
|
@halter73 can you help guide me towards next steps please? |
@halter73, just pinging this issue again to see what the next step is. |
I really don't want to be annoying about this issue, I just want to make sure that it isn't waiting on anything from me. Would it be helpful to consolidate the original API proposal and subsequent discussion into a single doc for API review? Alternatively, the draft PR (#55069) represents my thinking pretty well, and I created it as a draft because I have not written automated tests. I held off on that thinking that there might be more discussion on this thread, but I can write tests and get the PR ready for review if that is a better way forward. |
Sorry for the delay. I really do want to get this in for .NET 9, and the PR looks good! Feel free to start on the tests. I think those will be easy to update even if we adjust the API.
Yes please. Make sure to include details about the I think it might be a good idea to give a way for developers to say use pushed authorization if it's supported by the OIDC provider. Arguably, that should be the default behavior, so it'd be nice if the updated API proposal accounted for that. Please continue using the API proposal template you used in the initial proposal but copy it into a new comment on this thread. Don't worry about repeating stuff. It's nice to see the progress of the API design. If you could include a super simple example of someone using I see you already submitted AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#2499 to make the proposed We plan to review this next Thursday if that works for you. Thanks again! |
Updated API Proposal: Background and MotivationPushed Authorization Requests (PAR) is a relatively new OAuth standard that improves the security of OAuth and OIDC flows by moving authorization parameters from the front channel to the back channel (that is, from redirect URLs in the browser to direct machine to machine http calls on the back end). This prevents an attacker in the browser from
Pushing the authorization parameters also keeps request URLs short. Authorize parameters might get very long when using more complex OAuth and OIDC features such as Rich Authorization Requests, and URLs that are long cause issues in many browsers and networking infrastructure. The use of PAR is encouraged by the FAPI working group within the OpenID Foundation. For example, the FAPI2.0 Security Profile requires the use of PAR. This security profile is used by many of the groups working on open banking (primarily in Europe), in health care, and in other industries with high security requirements. PAR is supported by a number of identity providers, including
This proposal aims to add support for PAR to the OidcHandler. Proposed APIConfig FlagAdd a flag to the options to enable the behavior: namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
public class OpenIdConnectOptions
{
+ /// <summary>
+ /// Flag to set whether the handler should push authorization parameters on the backchannel before
+ /// redirecting to the identity provider.
+ /// The default is 'true'.
+ /// </summary>
+ public bool UsePushedAuthorization { get; set; } = true;
} When enabled, the parameters that the handler would normally set in the URL when it redirects to the authorize endpoint will instead be sent on a backchannel call to the PAR endpoint, and the resulting request_uri will instead be attached to the redirect, along with the client_id, as per the specification. Note that there is ongoing discussion about exactly how we ought to go about enabling this feature, especially given the way that this interacts with what discovery tells us. My current proposal is to enable the config flag by default, and then enable the feature if either of these conditions holds:
Event For ExtensibilityAdd an event that will be invoked before authorization parameters are pushed to facilitate extensibility. Event implementors can
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
public class OpenIdConnectEvents
{
+ /// <summary>
+ /// Invoked before authorization parameters are pushed using PAR.
+ /// </summary>
+ public Func<PushedAuthorizationContext, Task> OnPushAuthorization { get; set; } = context => Task.CompletedTask;
+ /// <summary>
+ /// Invoked before authorization parameters are pushed during PAR.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public virtual Task PushAuthorization(PushedAuthorizationContext context) => OnPushAuthorization(context);
} The If the event wants to control the behavior of the handler more, there are methods on the context to indicate this intent, similar to many of the other contexts (e.g., To control client authentication, there is To skip PAR entirely, there is To control pushing to the PAR endpoint, there is + /// <summary>
+ /// A context for <see cref="OpenIdConnectEvents.PushAuthorization(PushedAuthorizationContext)"/>.
+ /// </summary>
+ public class PushedAuthorizationContext : PropertiesContext<OpenIdConnectOptions>
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="PushedAuthorizationContext"/>.
+ /// </summary>
+ /// <inheritdoc />
+ public PushedAuthorizationContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage parRequest, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ {
+ ProtocolMessage = parRequest;
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdConnectMessage"/> that will be sent to the PAR endpoint.
+ /// </summary>
+ public OpenIdConnectMessage ProtocolMessage { get; }
+
+ /// <summary>
+ /// Indicates if the OnPushAuthorization event chose to handle pushing the
+ /// authorization request. If true, the handler will not attempt to push the
+ /// authorization request, and will instead use the RequestUri from this
+ /// event in the subsequent authorize request.
+ /// </summary>
+ public bool HandledPush { [MemberNotNull("RequestUri")] get; private set; }
+
+ /// <summary>
+ /// Tells the handler that the OnPushAuthorization event has handled the process of pushing
+ /// authorization, and that the handler should use the provided request_uri
+ /// on the subsequent authorize call.
+ /// </summary>
+ public void HandlePush(string requestUri)
+ {
+ HandledPush = true;
+ RequestUri = requestUri;
+ }
+
+ /// <summary>
+ /// Indicates if the OnPushAuthorization event chose to skip pushing the
+ /// authorization request. If true, the handler will not attempt to push the
+ /// authorization request, and will not use pushed authorization in the
+ /// subsequent authorize request.
+ /// </summary>
+ public bool SkippedPush { get; private set; }
+
+ /// <summary>
+ /// The request_uri parameter to use in the subsequent authorize call, if
+ /// the OnPushAuthorization event chose to handle pushing the authorization
+ /// request, and null otherwise.
+ /// </summary>
+ public string? RequestUri { get; private set; }
+
+ /// <summary>
+ /// Tells the handler to skip pushing authorization entirely. If this is
+ /// called, the handler will not use pushed authorization on the subsequent
+ /// authorize call.
+ /// </summary>
+ public void SkipPush()
+ {
+ SkippedPush = true;
+ }
+
+ /// <summary>
+ /// Indicates if the OnPushAuthorization event chose to handle client
+ /// authentication for the pushed authorization request. If true, the
+ /// handler will not attempt to set authentication parameters for the pushed
+ /// authorization request.
+ /// </summary>
+ public bool HandledClientAuthentication { get; private set; }
+
+ /// <summary>
+ /// Tells the handler to skip setting client authentication properties for
+ /// pushed authorization. The handler uses the client_secret_basic
+ /// authentication mode by default, but the OnPushAuthorization event may
+ /// replace that with an alternative authentication mode, such as
+ /// private_key_jwt.
+ /// </summary>
+ public void HandleClientAuthentication() => HandledClientAuthentication = true;
+ } Usage ExamplesBasic Usage services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://demo.duendesoftware.com";
options.ClientId = "interactive.confidential";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.UsePkce = true;
options.UsePushedAuthorization = true; // <------ New flag on
}) Customized Authentication services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://demo.duendesoftware.com";
o.ClientId = "interactive.confidential.jwt";
// Using a private_key_jwt instead of a client secret
o.EventsType = typeof(PrivateKeyJwtParEvents);
options.ResponseType = "code";
options.UsePkce = true;
options.UsePushedAuthorization = true; // <------ New flag on
}) public class PrivateKeyJwtParEvents(AssertionService assertionService)
{
public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
context.TokenEndpointRequest.ClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
context.TokenEndpointRequest.ClientAssertion = assertionService.CreateClientToken();
return Task.CompletedTask;
}
public override Task PushAuthorization(PushedAuthorizationContext context)
{
context.HandleClientAuthentication();
context.ProtocolMessage.ClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
context.ProtocolMessage.ClientAssertion = assertionService.CreateClientToken(); //
return Task.CompletedTask;
}
} public class AssertionService
{
private static string rsaKey =
"""
{
"d":"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ",
"dp":"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE",
"dq":"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M",
"e":"AQAB",
"kid":"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA",
"kty":"RSA",
"n":"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw",
"p":"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE",
"q":"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts",
"qi":"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4"
}
""";
public string CreateClientToken()
{
var now = DateTime.UtcNow;
var epochNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var token = new JwtSecurityToken(
"interactive.confidential.jwt",
"https://demo.duendesoftware.com/connect/token",
new List<Claim>()
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Sub, "interactive.confidential.jwt"),
new Claim(JwtRegisteredClaimNames.Iat, epochNow.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)
},
now,
now.AddMinutes(1),
new SigningCredentials(new JsonWebKey(rsaKey), "RS256")
);
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.OutboundClaimTypeMap.Clear();
return tokenHandler.WriteToken(token);
}
} Alternative DesignsInstead of adding support for PAR directly to the handler, we could instead allow users of the handler to implement PAR themselves within events. The challenge with the events is that the best currently existing event to use for that purpose is the
These two approaches are demonstrated here. RisksThis is a low risk change. Users would have to be using an AS that supports PAR to get the new behavior, and they have a flag to opt out. There are some authorization servers that support PAR, but not for all clients. We might want to consider falling back to passing over the front channel if PAR fails. We are making a new outgoing HTTP request to implement this specification, but we're making the request to a url that was supplied by the OIDC authority through the discovery document or directly by the developer using the explicit configuration in the handler. |
[API Review]
Proposed enum: namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
+ public enum PushedAuthorizationBehavior
+ {
+ Disable,
+ UseIfAvailable,
+ Require,
+ }
public class OpenIdConnectOptions
{
+ /// <summary>
+ /// Enum to set whether the handler should push authorization parameters on the backchannel before
+ /// redirecting to the identity provider.
+ /// The default is 'PushedAuthorizationBehavior.Enabled'.
+ /// </summary>
+ public PushedAuthorizationBehavior PushedAuthorizationBehavior { get; set; } = PushedAuthorizationBehavior.Enabled;
} |
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
+ public enum PushedAuthorizationBehavior
+ {
+ UseIfAvailable, // Default
+ Disable,
+ Require,
+ }
public class OpenIdConnectOptions
{
+ /// <summary>
+ /// Flag to set whether the handler should push authorization parameters on the backchannel before
+ /// redirecting to the identity provider.
+ /// <value>
+ /// The default value is <see cref="PushedAuthorizationBehavior.UseIfAvailable"/>.
+ /// </value>
+ /// </summary>
+ public PushedAuthorizationBehavior PushedAuthorizationBehavior { get; set; } = PushedAuthorizationBehavior.UseIfAvailable;
} namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
public class OpenIdConnectEvents
{
+ /// <summary>
+ /// Invoked before authorization parameters are pushed using PAR.
+ /// </summary>
+ public Func<PushedAuthorizationContext, Task> OnPushAuthorization { get; set; } = context => Task.CompletedTask;
+ /// <summary>
+ /// Invoked before authorization parameters are pushed during PAR.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public virtual Task PushAuthorization(PushedAuthorizationContext context) => OnPushAuthorization(context);
} + /// <summary>
+ /// A context for <see cref="OpenIdConnectEvents.PushAuthorization(PushedAuthorizationContext)"/>.
+ /// </summary>
+ public sealed class PushedAuthorizationContext : PropertiesContext<OpenIdConnectOptions>
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="PushedAuthorizationContext"/>.
+ /// </summary>
+ /// <inheritdoc />
+ public PushedAuthorizationContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage parRequest, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ {
+ ProtocolMessage = parRequest;
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdConnectMessage"/> that will be sent to the PAR endpoint.
+ /// </summary>
+ public OpenIdConnectMessage ProtocolMessage { get; }
+
+ /// <summary>
+ /// Indicates if the OnPushAuthorization event chose to handle pushing the
+ /// authorization request. If true, the handler will not attempt to push the
+ /// authorization request, and will instead use the RequestUri from this
+ /// event in the subsequent authorize request.
+ /// </summary>
+ public bool HandledPush { [MemberNotNull("RequestUri")] get; private set; }
+
+ /// <summary>
+ /// Tells the handler that the OnPushAuthorization event has handled the process of pushing
+ /// authorization, and that the handler should use the provided request_uri
+ /// on the subsequent authorize call.
+ /// </summary>
+ public void HandlePush(string requestUri)
+ {
+ HandledPush = true;
+ RequestUri = requestUri;
+ }
+
+ /// <summary>
+ /// Indicates if the OnPushAuthorization event chose to skip pushing the
+ /// authorization request. If true, the handler will not attempt to push the
+ /// authorization request, and will not use pushed authorization in the
+ /// subsequent authorize request.
+ /// </summary>
+ public bool SkippedPush { get; private set; }
+
+ /// <summary>
+ /// The request_uri parameter to use in the subsequent authorize call, if
+ /// the OnPushAuthorization event chose to handle pushing the authorization
+ /// request, and null otherwise.
+ /// </summary>
+ public string? RequestUri { get; private set; }
+
+ /// <summary>
+ /// Tells the handler to skip pushing authorization entirely. If this is
+ /// called, the handler will not use pushed authorization on the subsequent
+ /// authorize call.
+ /// </summary>
+ public void SkipPush()
+ {
+ SkippedPush = true;
+ }
+
+ /// <summary>
+ /// Indicates if the OnPushAuthorization event chose to handle client
+ /// authentication for the pushed authorization request. If true, the
+ /// handler will not attempt to set authentication parameters for the pushed
+ /// authorization request.
+ /// </summary>
+ public bool HandledClientAuthentication { get; private set; }
+
+ /// <summary>
+ /// Tells the handler to skip setting client authentication properties for
+ /// pushed authorization. The handler uses the client_secret_basic
+ /// authentication mode by default, but the OnPushAuthorization event may
+ /// replace that with an alternative authentication mode, such as
+ /// private_key_jwt.
+ /// </summary>
+ public void HandleClientAuthentication() => HandledClientAuthentication = true;
+ } |
API approved! |
Background and Motivation
Pushed Authorization Requests (PAR) is a relatively new OAuth standard that improves the security of OAuth and OIDC flows by moving authorization parameters from the front channel to the back channel (that is, from redirect URLs in the browser to direct machine to machine http calls on the back end).
This prevents an attacker in the browser from
Pushing the authorization parameters also keeps request URLs short. Authorize parameters might get very long when using more complex OAuth and OIDC features such as Rich Authorization Requests, and URLs that are long cause issues in many browsers and networking infrastructure.
The use of PAR is encouraged by the FAPI working group within the OpenID Foundation. For example, the FAPI2.0 Security Profile requires the use of PAR. This security profile is used by many of the groups working on open banking (primarily in Europe), in health care, and in other industries with high security requirements.
PAR is supported by a number of identity providers, including
This proposal aims to add support for PAR to the OidcHandler.
Proposed API
We need a new flag to enable the behavior:
And we need new properties on the
OpenIdConnectConfiguration
to describe the PAR-specific information we get from discovery (and use for manual configuration).When the
UsePushedAuthorization
flag is enabled in configuration or therequire_pushed_authorization_requests
flag is enabled in the discovery document or the explicit configuration, the parameters that the handler would normally set in the URL when it redirects to the authorize endpoint will instead be sent on a backchannel call to the PAR endpoint, and the resulting request_uri will instead be attached to the redirect, along with the client_id, as per the specification.It is an error to attempt to enable
UsePushedAuthorization
with an identity provider that publishes a discovery document without apushed_authorization_request_endpoint
(which indicates that the server does not support PAR) or with explicit configuration that does not set thePushedAuthorizationRequestEndpoint
.It is also an error to attempt to disable
UsePushedAuthorization
with an identity provider that publishes a discovery document that enablesrequire_pushed_authorization_requests
or with explicit configuration that enables theRequirePushedAuthorizationRequests
flag.Usage Examples
Alternative Designs
Instead of adding support for PAR directly to the handler, we could instead allow users of the handler to implement PAR themselves within events. The challenge with the events is that the best currently existing event to use for that purpose is the
OnRedirectToIdentityProvider
event, and it is raised before the state parameter is computed. There are a couple of ways that I see to work around this:OnRedirectToIdentityProvider
, and this would also happen when redirecting to the identity provider, just later in the process. If such an even existed, then users of the handler could add use that event to push the authorization parameters and change the redirect url, without needing to copy large amounts of code from the handler into their event.These two approaches are demonstrated here.
Risks
This is a low risk change. Users would have to opt in to get the new behavior. Existing Oidc scheme configurations are unaffected.
We are making a new outgoing HTTP request to implement this specification, but we're making the request to a url that was supplied by the OIDC authority through the discovery document or directly by the developer using the explicit configuration in the handler.
The text was updated successfully, but these errors were encountered: