Skip to main content

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.

Metadata

Release files for atproto-oauth-py 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for atproto-oauth-py 0.1.0
File Size Uploaded Provenance Actions
atproto_oauth_py-0.1.0.tar.gz 12.1 kB With provenance Details for atproto_oauth_py-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for atproto-oauth-py 0.1.0
File Interpreter ABI Platform Actions
atproto_oauth_py-0.1.0-py3-none-any.whl Python 3 none any Details for atproto_oauth_py-0.1.0-py3-none-any.whl

Total release size: 24.9 kB

Release files / atproto_oauth_py-0.1.0.tar.gz

Download URL atproto_oauth_py-0.1.0.tar.gz
Size 12.1 kB
Tags Source
SHA-256 checksum
How to use checksums
9e5083a4fb47449119362dd2008d9e2a6e0218c613123dc570547a6d04a7c597
BLAKE2b-256 checksum
How to use checksums
67f8ac58077457918f291cf38b2413a1c18f2f1e53dc66f7aa9a39bd5aa6597c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 30, 2026.

Transparency log

Release files / atproto_oauth_py-0.1.0-py3-none-any.whl

Download URL atproto_oauth_py-0.1.0-py3-none-any.whl
Size 12.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
24da4b758378ed958357dcbf97e2e5351c4082d44e86a76ed706eb2e1b504aaf
BLAKE2b-256 checksum
How to use checksums
7e59a15627fe86599ddfc1a8f70c6833ce604955dde4c45a899a5e7c4d902086
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 30, 2026.

Transparency log

Security for atproto-oauth-py 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for atproto_oauth_py-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page