requests_oauth2client icon indicating copy to clipboard operation
requests_oauth2client copied to clipboard

Support protected resource metadata endpoints

Open snarfed opened this issue 10 months ago • 4 comments

  • requests_oauth2client version: head
  • Python version: 3.12.8
  • Operating System: macOS

Description

Hi again! The service I'm building an OAuth client for, Bluesky, uses a protected resource metadata endpoint. That RFC is still just a draft, but it came out of the OAuth working group, and afaik is pretty close to accepted. I'm handling the protected resource endpoint myself, with code below, but eventually it'd be nice if requests_oauth2client supported it natively!

PROTECTED_RESOURCE_PATH = '/.well-known/oauth-protected-resource'
RESOURCE_METADATA_PATH = '/.well-known/oauth-authorization-server'

...
resp = util.requests_get(urljoin(pds_url, PROTECTED_RESOURCE_PATH))
resp.raise_for_status()
auth_server = resp.json()['authorization_servers'][0]

client = OAuth2Client.from_discovery_endpoint(
  urljoin(auth_server, RESOURCE_METADATA_PATH),
  ...

snarfed avatar Feb 14 '25 00:02 snarfed

Integrated support for this is definitely on my TODO list.

BTW, you can already do something like this with the well_known_uri helper method:

from requests_oauth2client import well_known_uri, oauth2_discovery_document_url, OAuth2Client

resp = util.requests_get(well_known_uri(pds_url, "oauth-protected-resource"))
resp.raise_for_status()

auth_server = resp.json()['authorization_servers'][0]

client = OAuth2Client.from_discovery_endpoint(issuer=auth_server) # using `issuer` kwargs, OAuth2Client will go for an "openid-configuration" well-known uri
# or
client = OAuth2Client.from_discovery_endpoint(oauth2_discovery_document_url(auth_server) # or explicitly provide the full url, with another helper method

guillp avatar Feb 17 '25 15:02 guillp

RFC9728 is now released. I have implemented initial support for this, feel free to review the PR.

However, looking back at your code above which chooses an authorization server arbitrarily by just picking the first one in the authorization_servers list: I don't think that is a good idea security wise. If the RS is compromised, it will force your client to send its credentials (client_id and typically client_secret) to the url of its choice. You did not include those credentials in your code, but since you need to obtain them as prerequisite to calling the API, you already know which AS issuer you are going to use anyway.

That's why in my current implementation, you need to initialize your client with a trusted AS before you try to initialize your API.

I don't know of any real-life scenario where a fully dynamic AS discovery at runtime makes sense anyway, but please feel free to prove me wrong.

guillp avatar May 02 '25 13:05 guillp

Thanks! Discovery for Bluesky is indeed pretty dynamic, it's a decentralized network, so I won't know all possible ASes ahead of time. Correspondingly, Bluesky OAuth doesn't use client_secret, it always uses/requires PKCE instead. From https://atproto.com/specs/oauth :

Unlike a centralized app platform, in atproto there are many independent server implementations, so server discovery and client registration are automated using a combination of public auth server metadata and public client metadata. The client_id is a fully-qualified web URL pointing to the public client metadata (JSON document). There is no client_secret shared between servers and clients. When initiating a login with a handle or DID, an atproto-specific identity resolution step is required to discover the account’s PDS network location.

Also, from https://atproto.com/specs/oauth#authorization-servers :

Both Resource Servers (PDS instances) and Authorization Servers (PDS or entryway) need to publish metadata files at well-known HTTPS endpoints.

Resource Server (PDS) metadata must comply with the "OAuth 2.0 Protected Resource Metadata" (draft-ietf-oauth-resource-metadata) draft specification. A summary of requirements:

  • ...
  • must contain an authorization_servers array of strings, with a single element, which is a fully-qualified URL

The modern OAuth details here are over my head a bit, I'm sure you understand all this much better than me. I could definitely fail fast if I ever see a Bluesky protected resource endpoint with more than one element in authorization_servers, but I'm not sure what I can do beyond that. Any thoughts?

snarfed avatar May 02 '25 22:05 snarfed

Thanks for the insight of Bluesky specifications. I'm not familiar with that at all yet. I'll review that when I have some time and then will think about what can be done to improve support.

guillp avatar May 04 '25 19:05 guillp