Skip to content

Conversation

@zachswasey
Copy link

@zachswasey zachswasey commented Sep 1, 2025

This is a WIP to request feedback early on in the approach before I clean things up, write tests, and submit an official PR.

This implements RFC-9126: OAuth 2.0 Pushed Authorization Requests

I'm supporting ATProto (Bluesky) in my application, and their implementation requires DPoP and PAR. You can read more about their specific requirements here for context.

This is only the client-side code at the moment, but again just looking for overall feedback before cleaning up and adding tests.

It's not particularly complicated on the client-side to support, but working it into the existing framework and maintain backwards compatibility was a bit difficult which is mostly where feedback would be welcome. Since PAR accepts all the same values at /par as would've normally been sent to /authorize I re-utilized authorize_params for both use-cases, and added a separate authorize_url_params arg specifically for parameters that need to be sent to /authorize in both PAR and non-PAR use-cases. I couldn't think of a cleaner way to do this, or even a better name (always horrible at naming things!), so suggestions are welcome.

Server support has been added as well. For saving the generated request_uri I added it to the AuthorizationCode, but that complicates the existing query_authorization_code() and save_authorization_code() methods since they'd need to be expanded to include the state, request_uri, and expires_in. I'm not sure how y'all have handled breaking changes like that in the past, so feedback explicitly about how you'd like to handle that would be most useful.

TODO:

  • Tests
  • Server-side support

@azmeuk
Copy link
Member

azmeuk commented Sep 5, 2025

First of all, a big thank you for the effort you put in those specs. 🙇
I gave a quick look.

On the server side, the main issue I can see is that there is some RFC9126 code in the RFC6749 module (for example the create_pushed_authorization_response method, or the AuthorizationCodeMixin methods (get_code etc.)). The Authlib design is to keep the RFC specific code in dedicated modules and make them as non-intrusive as can be.
To achieve this you can register authorization server extensions andregister grant extensions, and then use hooks to trigger custom code at the right moment. You can also provide mixins and except developers to inherit them.
If needed you could define new hooks in rfc6749. All those mechanisms might not be enough to fill your need, in that case please tell us and we will think on how to find a better solution.

I'm not sure how y'all have handled breaking changes like that in the past, so feedback explicitly about how you'd like to handle that would be most useful.

Breaking changes could be managed if necessary with a proper deprecation period. However, Authlib should be usable with or without PAR. I am not yet sure that PAR induces breaking changes. I might be smooth if it used the things I mention in the comments.

"introspection_endpoint_auth_signing_alg_values_supported",
"code_challenge_methods_supported",
"pushed_authorization_request_endpoint",
"require_pushed_authorization_requests",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put the rfc9126 metadata in a dedicated class in rfc9126/discovery.py, like this is done with rfc9101 for example. Developers will then be able to use it with heritage:

class AuthorizationServerMetadata(
    rfc8414.AuthorizationServerMetadata,
    rfc9101.AuthorizationServerMetadata,
    rfc9126.AuthorizationServerMetadata,
):
    pass

I know that this is not well documented at the moment, and that rfc8414 features are limited.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha! Yeah, I really only added it because, well, other members were there. But I also noticed that actually creating an endpoint for rfc8414 doesn't seem to be included anywhere, or utilizing the validations on the client side. I had thoughts about putting up another PR for that, and being able to actually reference the metadata from within to determine behavior (like PAR enabled, or the DPoP algorithms). I assume that was the intention at some point?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I also noticed that actually creating an endpoint for rfc8414 doesn't seem to be included anywhere, or utilizing the validations on the client side.

Yes, at the moment the only feature of rfc8414 is that developers can validate their metadata before publishing it. There is probably more that could be done. See #260

I had thoughts about putting up another PR for that, and being able to actually reference the metadata from within to determine behavior

Other specs implementation already do this. For instance JAR asks developers to provide the server metadata dict and then use it to determine if features are supported:

# When the value of it [require_signed_request_object] as server metadata is true,
# then the server MUST reject the authorization request
# from any client that does not conform to this specification.
metadata = self.get_server_metadata()
if metadata and metadata.get("require_signed_request_object", False):
raise InvalidRequestError(
"Authorization requests for this server must use signed request objects.",
state=request.payload.state,
)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect. I'll take a closer look at how that file handles general authorization server extensions and apply it to the other PAR code, same with the server metadata. Ideally the get_server_metadata() should be abstracted out from any extension and made more central so it doesn't have to be re-implemented every time, and 8414 section 3.2 explicitly allows other claims from other specs, but I'll follow the pattern in 9101 for this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally the get_server_metadata() should be abstracted out from any extension and made more central so it doesn't have to be re-implemented every time

Yes, I had this in my though for some times now :) At some point this will probably be refactored/redesigned.

@azmeuk
Copy link
Member

azmeuk commented Sep 5, 2025

On the server-side, RFC9126 §3 client metada is missing. You can take inspiration of rfc9101/registration.py for example.

@azmeuk
Copy link
Member

azmeuk commented Sep 5, 2025

A detail: Mixing client code and server code in the same PR is totally fine. It would maybe be easier for the review if it was separated though (because one could be merged before the other one, and that would help focusing the discussions around dedicated business logic.)

@zachswasey
Copy link
Author

A detail: Mixing client code and server code in the same PR is totally fine. It would maybe be easier for the review if it was separated though (because one could be merged before the other one, and that would help focusing the discussions around dedicated business logic.)

Yeah, I didn't originally intend to do server code at all, but it's made testing a lot easier as I could test the DPoP and PAR features individually since Bluesky is the only public endpoint I knew of that supported them. The intention was always to just get some high level feedback on the best way to integrate into Authlib (which you've given so far!) because y'all take a more unique approach to structuring the library than other similar solutions, and then I'll clean up, add tests, and will definitely put up a separate client/server PR to be merged separately and with squashed commits, for more granular comments.

I'll tackle the suggestions later today!

par_required = metadata.get("require_pushed_authorization_requests")
if par_required and not par_endpoint:
raise RuntimeError('Missing "pushed_authorization_url" value')
par_required = par_required or par_endpoint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A detail, I would have called this var should_use_par instead.

This makes me think that this enforces the usage of PAR if available, and this is probably a good security practice. However, are there situations when one might want to avoid using PAR even if it is available on the server?

@zachswasey zachswasey mentioned this pull request Sep 5, 2025
8 tasks
@zachswasey
Copy link
Author

I refactored PAR to follow the same model as JAR (RFC9101), which made a whole lot more sense once I realized what that was doing. This, however, required a refactoring of JAR due to conflicts with both acting on request_uri and JAR raising exceptions early ending the flow. So I introduced a RequestURIExtension to allow multiple handlers (JAR and PAR right now) to plug in and reliably handle request_uris of different sources.

As a result of the refactoring of JAR I deprecated the old class (which was also erroneously named JWTAuthenticationRequest), and introduced the refactored JWTAuthorizationRequest. I also updated the discovery/registration a bit to be cleaner and better match the rest of the code-base. I believe ClientMetadataClaims also erroneously subclassed BaseClaims from JOSE, which doesn't make any sense, so fixed that too. It should be fixed in 7591 too, but I left that since it wasn't part of the code I was touching.

As before, please take a look at the high level approach and give feedback. Once we're aligned, I'll focus on cleaning up a few things (RequestURIExtension should catch exceptions, docs, etc), write tests, and push a new PR.

@zachswasey
Copy link
Author

Any chance I can get some feedback on this and #808 so I can close things out on my end?

@azmeuk
Copy link
Member

azmeuk commented Oct 7, 2025

Sorry I got busy elsewhere. I'll have a look in the week.

@azmeuk
Copy link
Member

azmeuk commented Oct 13, 2025

I did not find time last week, but I don't lose hope for this one 🤞

self.refresh_token = None
self.credential = None
self.endpoint = None
self.source = None
Copy link
Member

@azmeuk azmeuk Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are those parameters for?

if request_uri_data:
handler.handle_request_uri_data(request_uri_data, server, request)
return
raise InvalidRequestUriError(state=request.payload.state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This iteration is to be able to support the regular request_uri from rfc9101 where the request object is hosted by the client, at the same time than supporting PAR and the request_uri kept internally by the AS. Am I good?

@azmeuk
Copy link
Member

azmeuk commented Oct 14, 2025

It took me some time to read rfc9216 and rfc9101 but now I better see the big picture and how the two specs work together.
The PR looks good, the refactoring you propose look pertinent, the implementation is clean, there is not much coupling between modules. Nice work 👍
The refactoring in rfc9101 adds some complexity, but I am not sure how else can the request_uri parameter be managed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants