-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Support for Pushed Authorization (PAR) in OIDC Handler #55069
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
Conversation
Adds support for pushed authorization requests (RFC 9126) to the OpenIdConnect authentication handler.
@dotnet-policy-service agree company="Duende Software" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR. This helps a lot with the API review. I don't think I would have noticed the interaction between UsePushedAuthorization
and "require_pushed_authorization_requests" without looking at the code for example.
Sorry for the delay reviewing the corresponding API proposal. Feel free to start on the tests. I think those will be easy to update even if we adjust the API.
src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
Outdated
Show resolved
Hide resolved
|
||
var parRequired = ConfigFlagEnabled("require_pushed_authoriation_requests"); | ||
|
||
if (Options.UsePushedAuthorization || parRequired) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a way for the developer to say use pushed authorization if possible, but don't fail if there's no "pushed_authorization_request_endpoint" available? Maybe this should be the default behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect this support to be variable? E.g. an auth server will support it or not, you should be able to determine that during development. If that support changes later I don't know that we should react dynamically in prod to it as there may be side-effects such as your extension callbacks no longer being called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change in behavior seems small. It doesn't even seem to prevent any callbacks from being called. It does change the parameters used for the user agent's ultimate authorization request, but I don't see how that's likely to cause issues. I'd be more worried about the OIDC provider requiring a custom authentication scheme to use the PAR endpoint, but we could fall back to not using PAR if given a non-200 response if PAR wasn't explicitly opted into. We could even stop trying PAR if the first attempt fails.
I think it's very possible that an OIDC provider that didn't support PAR during initial development might add support later. I understand not wanting to risk any potential breaks caused by an OIDC provider lighting up a feature that's not actually required for us to use, but it feels like we should default to the more secure option if it's available and it's unlikely to break anyone.
@blowdart do you have an opinion on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An apps behavior should be in control of the developer, not a 3rd party, and shouldn't change without the developer initiating and testing that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OpenIdConnectHandler
lighting up features only if the .well-known/openid-configuration
indicates the OIDC provider supports it is not breaking new ground. If you set GetClaimsFromUserInfoEndpoint
to true, we will ignore it (aside from logging a debug-level message) if there is no "userinfo_endpoint".
aspnetcore/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
Lines 934 to 940 in 53db4d9
var userInfoEndpoint = _configuration?.UserInfoEndpoint; | |
if (string.IsNullOrEmpty(userInfoEndpoint)) | |
{ | |
Logger.UserInfoEndpointNotSet(); | |
return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); | |
} |
We can debate what the default should be, but I think it's reasonable to want to have a way to say if the OIDC provider supports a feature that should enhance security, use it. I could see somebody wanting to do this in a library or code analyzer that ensures PAR is used when available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that I've encountered is that some providers actually support PAR, but not for all clients. We ran into this when we added similar logic to IdentityModel.OidcClient (DuendeArchive/IdentityModel.OidcClient#428).
/// Initializes a new instance of <see cref="PushedAuthorizationContext"/>. | ||
/// </summary> | ||
/// <inheritdoc /> | ||
public PushedAuthorizationContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage message, HttpRequestMessage parRequest, AuthenticationProperties properties) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public PushedAuthorizationContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage message, HttpRequestMessage parRequest, AuthenticationProperties properties) | |
public PushedAuthorizationContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage message, AuthenticationProperties properties) |
If we think the PushedAuthorizationContext
needs the HttpRequestMessage
for some scenario, we should add a property for it instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've rearranged this, so we don't need a separate parameter at all. It turned out to be quite inconvenient to deal with the request message, because both the handler and the event might want to make changes to the parameters that will be sent.
@Tratcher Do you mind taking a look at this too? Thanks. |
src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs
Outdated
Show resolved
Hide resolved
var credential = $"{escapedClientId}:{escapedClientSecret}"; | ||
var encodedCredential = Convert.ToBase64String(Encoding.UTF8.GetBytes(credential)); | ||
|
||
parRequest.Headers.Authorization = new AuthenticationHeaderValue("basic", encodedCredential); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this header chosen for authentication by default? Looking at RFC 9126, it allows authenticating the same way you would with the token endpoint as defined in RFC 6749.
I don't see anything where the client ID and client secret are put into a basic auth header. The "introductory example" in RFC 9126 uses "JWT client assertion-based authentication" defined in RFC 7523, but that doesn't seem to provide a mechanism to acquire the JWT.
The common thing for the token endpoint seems to be to put the client ID and client secret in the request body of the token endpoint even though it's "NOT RECOMMENDED and SHOULD be limited to clients unable to directly utilize the HTTP Basic authentication scheme."
Why not just include the client secret in the parRequest
body via authorizeRequest.Parameters
since it's never seen by the client? It already includes the client ID, the spec seems to allow it, and it's effectively what we do by default for the token endpoint, right?
The request also includes, as appropriate for the given client, any additional parameters necessary for client authentication (e.g., client_secret or client_assertion and client_assertion_type)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right - I'll switch to the request body style.
I was trying to follow the "NOT RECOMMENDED" language, despite the inconsistency with the way we currently call the token endpoint. I had some thought that we might want to change that as well, but the latest draft of OAuth 2.1 actually drops this language:
Change client credentials to be required to be supported in the request body to avoid HTTP Basic authentication encoding interop issues
So that really makes it a moot point, and we should definitely just use the request body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having basic auth of any description would be rather problematic from a security POV :)
Content = new FormUrlEncodedContent(authorizeRequest.Parameters), | ||
}; | ||
var context = new PushedAuthorizationContext(Context, Scheme, Options, authorizeRequest, parRequest, properties); | ||
await Events.PushAuthorization(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that the authorizeRequest.Parameters
are already serialized into the FormUrlEncodedContent
by the time this event is called. I know that's probably because you want to allow the event to modify the HttpRequestMessage
, but I think we should set HttpRequestMessage.Content
after this event if it's still null.
It's probably also a good idea to give the event a chance to set parRequest.RequestUri
before failing due to missing "pushed_authorization_request_endpoint" from the configuration. I'm not sure how likely it is that anyone will need it, but it probably doesn't hurt to allow for that flexibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of allowing the event to modify the request message, I'm just passing an OpenIdConnectMessage
that the handler later pushes. With no http request message, we don't have to worry about its content being null.
The context now can affect the flow of control in the handler in 3 ways:
- If it handles authentication with
HandleClientAuthentication
, we skip setting the client secret - If it decides that we don't need to push with
SkipPush
, we completely skip pushing, and redirect as normal - If it decides to completely control the call to the par endpoint with
HandlePush
, the event won't make the par request and instead will use therequest_uri
passed toHandlePush
- The event can also tweak parameters before they are pushed, but not call any of the context's methods to get otherwise default behavior.
The downside to this is that it is possible to use the API in an ambiguous way: what should the handler do if the event calls both HandlePush
and SkipPush
? Should that be an error? A logged warning? Should the last call take precedence?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside to this is that it is possible to use the API in an ambiguous way: what should the handler do if the event calls both
HandlePush
andSkipPush
? Should that be an error? A logged warning? Should the last call take precedence?
I think throwing an InvalidOperationException
is a good choice if we detect this. I don't think it should depend on order.
src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
Outdated
Show resolved
Hide resolved
|
||
var parRequired = ConfigFlagEnabled("require_pushed_authoriation_requests"); | ||
|
||
if (Options.UsePushedAuthorization || parRequired) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change in behavior seems small. It doesn't even seem to prevent any callbacks from being called. It does change the parameters used for the user agent's ultimate authorization request, but I don't see how that's likely to cause issues. I'd be more worried about the OIDC provider requiring a custom authentication scheme to use the PAR endpoint, but we could fall back to not using PAR if given a non-200 response if PAR wasn't explicitly opted into. We could even stop trying PAR if the first attempt fails.
I think it's very possible that an OIDC provider that didn't support PAR during initial development might add support later. I understand not wanting to risk any potential breaks caused by an OIDC provider lighting up a feature that's not actually required for us to use, but it feels like we should default to the more secure option if it's available and it's unlikely to break anyone.
@blowdart do you have an opinion on this?
I think it's very possible that an OIDC provider that didn't support PAR during initial development might add support later. I understand not wanting to risk any potential breaks caused by an OIDC provider lighting up a feature that's not actually required for us to use, but it feels like we should default to the more secure option if it's available and it's unlikely to break anyone. We should be more secure by default, and if that breaks people give them an opt-out |
Just to give you a heads up, code complete for preview 6 is this Wednesday. I know that's very soon, so I'm fine with waiting for preview 7 which is scheduled to be code complete July 19th. I think that's the latest we'd take this change. |
Remember, code complete for the last preview is this Friday, July 19th. |
You can indicate if the event handled authentication, handled the push entirely, or opted out of pushing. Doing more than 1 of those is at best redundant but often ambiguous, so it is always disallowed.
@halter73 I marked this as ready for review last night, then woke up and realized I wanted to do a bit more cleanup and handle the error case of the event calling multiple context methods (e.g., you shouldn't be able to both say that pushing is skipped, but also you handled the authentication part of the push). That's all in now, and it is really, truly, for-real-this-time ready for review. Thanks! |
<PropertyGroup> | ||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> | ||
<UserSecretsId>aspnet5-OpenIdConnectSample-20151210110318</UserSecretsId> | ||
<UserSecretsId>aspnet5-OpenIdConnectSample-20151210110318</UserSecretsId> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<UserSecretsId>aspnet5-OpenIdConnectSample-20151210110318</UserSecretsId> | |
<UserSecretsId>aspnet5-OpenIdConnectSample-20151210110318</UserSecretsId> |
src/Security/Authentication/OpenIdConnect/src/Events/PushedAuthorizationContext.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
// TODO GetConfigString and ConfigFlagEnabled are only used in PAR, and won't be necessary after | ||
// Microsoft.IdentityModel.Protocols.OpenIdConnect is updated to v7.6.1 or later. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've updated to 8.0.0, so we should be able to use PushedAuthorizationRequestEndpoint
and the like now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, I switched to it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, how are the dependencies managed? The build system is ... well it's a lot for the uninitiated 😄
I found eng/BaselineDesigner.props, but it has this:
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Authentication.JwtBearer' AND ('$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' OR '$(TargetFramework)' == 'net8.0') ">
<BaselinePackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="[7.0.3, )" />
</ItemGroup>
That looks like 7.0.3 is being referenced, which is clearly not the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dependency versions we use come from Version.props. BaselineDesigner.props is to catch unintentional breaking changes in servicing builds. This should provide some more context.
aspnetcore/eng/targets/ResolveReferences.targets
Lines 11 to 17 in dfc5189
Items used by the resolution strategy: | |
* BaselinePackageReference = a list of packages that were referenced in the last release of the project currently building | |
- mainly used to ensure references do not change in servicing builds unless $(UseLatestPackageReferences) is not true. | |
* LatestPackageReference = a list of the latest versions of packages | |
* Reference = a list of the references which are needed for compilation or runtime | |
* ProjectReferenceProvider = a list which maps of assembly names to the project file that produces it |
src/Security/Authentication/OpenIdConnect/src/PushedAuthorizationBehavior.cs
Outdated
Show resolved
Hide resolved
src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
Outdated
Show resolved
Hide resolved
src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
Outdated
Show resolved
Hide resolved
…dler.cs Co-authored-by: Stephen Halter <[email protected]>
…ionBehavior.cs Co-authored-by: Stephen Halter <[email protected]>
…dler.cs Co-authored-by: Stephen Halter <[email protected]>
…pnetcore into josephdecock/par
@javiercn could you please retry the pipeline failures? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
var requestMessage = new HttpRequestMessage(HttpMethod.Post, parEndpoint); | ||
requestMessage.Content = new FormUrlEncodedContent(parRequest.Parameters); | ||
requestMessage.Version = Backchannel.DefaultRequestVersion; | ||
var parResponseMessage = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that I've encountered is that some providers actually support PAR, but not for all clients. We ran into this when we added similar logic to IdentityModel.OidcClient (DuendeArchive/IdentityModel.OidcClient#428).
I just noticed this from the previous thread about making UseIfAvailable
the default. I was also concerned that there may be providers that authenticate the PAR request with something other than the client_secret parameter. Would it make sense to log a warning and fallback to a normal non-pushed authorization attempt if we cannot get a PAR request URI in the UseIfAvailable
case?
I'm still fine with merging this in a preview to see how it goes, but not many people use previews compared to major releases. It might be a good idea for us to update this behavior in RC1 even if no one complains to be extra safe, but I'm curious what everyone else thinks about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the one hand, in the issue I linked to in IdentityModel.OidcClient, the issue was for public clients specifically and most users of the OidcHandler will be confidential clients.
On the other hand, if you have customized the behavior of the handler to call the token endpoint using a different form of authentication, presumably you need to do the same for the PAR requests that UseIfAvailable will start making for you.
But, users who are customizing the handler's authentication already are advanced users and personally I'm okay with expecting them to sort this out.
My feeling is to keep the UseIfAvailable default. For RC1, we could take a look at exactly what the error message is if authentication to the PAR endpoint fails and log a hint about the new APIs.
Support for Pushed Authorization Requests
Adds support for Pushed Authorization Requests (RFC 9126) to the OIDC authentication handler.