Skip to content

Support protected resource metadata endpoints #121

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

Open
snarfed opened this issue Feb 14, 2025 · 4 comments · May be fixed by #145
Open

Support protected resource metadata endpoints #121

snarfed opened this issue Feb 14, 2025 · 4 comments · May be fixed by #145

Comments

@snarfed
Copy link
Contributor

snarfed commented Feb 14, 2025

  • 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),
  ...
@guillp
Copy link
Owner

guillp commented Feb 17, 2025

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 pushed a commit that referenced this issue May 2, 2025
@guillp
Copy link
Owner

guillp commented May 2, 2025

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 guillp linked a pull request May 2, 2025 that will close this issue
@snarfed
Copy link
Contributor Author

snarfed commented May 2, 2025

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?

@guillp
Copy link
Owner

guillp commented May 4, 2025

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.

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 a pull request may close this issue.

2 participants