Skip to main content

AT Protocol OAuth 2.1 + DPoP library and CLI: handle/DID/PDS resolution, PAR, PKCE, refresh-token flow, and DPoP-bound resource calls.

Project description

atproto-oauth-py

AT Protocol OAuth 2.1 + DPoP library and CLI for Python. Handles handle/DID/PDS resolution, PAR (Pushed Authorization Requests), PKCE, refresh-token rotation, and DPoP-bound HTTP calls.

Status

Works end-to-end against the OAuth flow: init resolves a handle through to a PDS-issued authorization URL, complete exchanges an authorization code for a refresh token + DPoP private key. The library also exposes runtime DPoP helpers for making authenticated calls.

There is one important caveat for the BlueSky use case (see Limitations below): BlueSky's AppView at bsky.social rejects OAuth tokens minted by third-party PDSes with "OAuth tokens are meant for PDS access only". OAuth is fine for talking to your own PDS; cross-PDS AppView calls require service-auth or app-password authentication instead.

Install

pip install atproto-oauth-py

CLI: one-time auth

The CLI is two-phase. Between the phases, you open a browser, authorize, and paste back the redirect URL.

You also need a publicly-hosted client metadata JSON document and a callback page at the redirect URI. Templates are included under oauth-templates/ — copy them to your hosting and edit the URLs to match your domain.

# Phase 1: build the authorization URL.
export ATPROTO_OAUTH_CLIENT_ID=https://example.com/oauth/client-metadata.json
export ATPROTO_OAUTH_REDIRECT_URI=https://example.com/oauth/callback/
atproto-oauth init alice.bsky.social

This prints an authorization URL. Open it in a browser, sign in to your PDS, authorize the request. You'll land on the callback page, which displays the full URL it received. Copy that URL.

# Phase 2: exchange the auth code for tokens.
atproto-oauth complete 'https://example.com/oauth/callback/?code=...&state=...&iss=...'

This prints four credentials to add to your secrets store:

ATPROTO_OAUTH_REFRESH_TOKEN=...
ATPROTO_OAUTH_DPOP_PRIVATE_JWK={"kty":"EC","crv":"P-256",...}
ATPROTO_OAUTH_PDS_ISSUER=https://your-pds
ATPROTO_OAUTH_DID=did:plc:...

Phase 1 persists transient state to .atproto-oauth-state.json (gitignored); phase 2 reads and consumes it.

Library: runtime DPoP-bound calls

Once you have the four credentials above, you can call your PDS:

import json, os
from atproto_oauth.dpop import dpop_post_form, dpop_get, jwk_to_key, public_jwk
from atproto_oauth.flow import discover_token_endpoint

private_jwk = json.loads(os.environ["ATPROTO_OAUTH_DPOP_PRIVATE_JWK"])
private_key = jwk_to_key(private_jwk)
pub_jwk = public_jwk(private_jwk)

# Refresh access token.
token_endpoint = discover_token_endpoint(os.environ["ATPROTO_OAUTH_PDS_ISSUER"])
tokens = dpop_post_form(
    token_endpoint,
    {
        "grant_type": "refresh_token",
        "refresh_token": os.environ["ATPROTO_OAUTH_REFRESH_TOKEN"],
        "client_id": os.environ["ATPROTO_OAUTH_CLIENT_ID"],
    },
    private_key, pub_jwk,
)
access_token = tokens["access_token"]

# Watch for refresh_token rotation; if tokens["refresh_token"] != the one you
# sent, persist the new one before the next call.

# Now make a DPoP-bound call.
r = dpop_get(
    f"{os.environ['ATPROTO_OAUTH_PDS_ISSUER']}/xrpc/com.atproto.repo.describeRepo",
    access_token, private_key, pub_jwk,
    params={"repo": os.environ["ATPROTO_OAUTH_DID"]},
)
print(r.json())

Hosting the client metadata

OAuth 2.1 for AT Protocol uses a publicly-hosted JSON document as the client_id. Copy oauth-templates/client-metadata.json to your web host (GitHub Pages, Netlify, S3, etc.), edit the URLs, and serve it at a stable URL that becomes your ATPROTO_OAUTH_CLIENT_ID.

oauth-templates/callback/index.html is the page that receives the authorization redirect — it just displays the URL it was loaded with so you can copy it back to phase 2.

Both files have no server-side logic; static hosting is fine.

Limitations

  • Tested against: bsky.social and eurosky.social PDSes. Other AT Protocol PDSes that expose /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server should work but haven't been verified.
  • AppView interop: BlueSky's AppView (bsky.social's app.bsky.* endpoints) explicitly rejects OAuth-minted access tokens with "OAuth tokens are meant for PDS access only". This is a BlueSky policy, not a fix-able bug. OAuth tokens work for talking to your own PDS; for AppView calls you need service-auth (which only works for bsky.social-hosted accounts in practice) or an app password.
  • Single-user: there is no token store, no multi-account support, and no encrypted-at-rest persistence. The library returns the credentials and you decide where to put them (env vars, secret manager, etc.).

License

MIT. See LICENSE.

Provenance

Extracted from https://github.com/tenorune/tenorune.github.io's scripts/ directory, where it powered the OAuth scaffolding for the Stories of 47 archive's BlueSky save ingestion. The runtime ingestion lives in bsky-saves.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

atproto_oauth_py-0.1.0.tar.gz (12.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

atproto_oauth_py-0.1.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file atproto_oauth_py-0.1.0.tar.gz.

File metadata

  • Download URL: atproto_oauth_py-0.1.0.tar.gz
  • Upload date:
  • Size: 12.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for atproto_oauth_py-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9e5083a4fb47449119362dd2008d9e2a6e0218c613123dc570547a6d04a7c597
MD5 1b49e5820254250c7fe27aa5371ab257
BLAKE2b-256 67f8ac58077457918f291cf38b2413a1c18f2f1e53dc66f7aa9a39bd5aa6597c

See more details on using hashes here.

Provenance

The following attestation bundles were made for atproto_oauth_py-0.1.0.tar.gz:

Publisher: release.yml on tenorune/atproto-oauth-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file atproto_oauth_py-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for atproto_oauth_py-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24da4b758378ed958357dcbf97e2e5351c4082d44e86a76ed706eb2e1b504aaf
MD5 71341f28c46ae413d4592f3e496138bd
BLAKE2b-256 7e59a15627fe86599ddfc1a8f70c6833ce604955dde4c45a899a5e7c4d902086

See more details on using hashes here.

Provenance

The following attestation bundles were made for atproto_oauth_py-0.1.0-py3-none-any.whl:

Publisher: release.yml on tenorune/atproto-oauth-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page