-
-
Notifications
You must be signed in to change notification settings - Fork 76
Add OAuth 2.1 implementation for ATProto SDK #636
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
Draft
zzstoatzz
wants to merge
11
commits into
MarshalX:main
Choose a base branch
from
zzstoatzz:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
+2,770
−2
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Implements complete OAuth 2.1 support following the ATProto OAuth specification (https://atproto.com/specs/oauth), building on concepts from PR MarshalX#589. Features: - Full OAuth 2.1 authorization code flow with PKCE (S256) and DPoP (ES256) - Pushed Authorization Requests (PAR) - DID-based authentication with handle/DID resolution - PDS endpoint and authorization server discovery - Automatic DPoP nonce rotation - Client assertions for confidential clients - Pluggable state and session stores - SSRF protection and security best practices Components: - New package: atproto_oauth with complete OAuth client - PKCE manager for code challenge generation - DPoP manager for JWT proof generation - Security utilities with URL validation - Metadata discovery for auth servers and PDS - Abstract and in-memory stores for state/sessions - 12 unit tests (all passing) - Flask reference implementation - Comprehensive documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Update exceptions to inherit from AtProtocolError for consistency - Remove unused DPoPNonceError exception - Clean up comment wording in revoke_session - Add spec compliance documentation - Update ruff ignore rules for intentional patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
**Bug 1: Identity Resolution API** - Fixed incorrect use of identity resolver API (client.py:77) - Was trying to unpack tuple from did.resolve() which only returns Optional[DidDocument] - Now properly resolves handles to DIDs first, then resolves DID document - Uses resolve_atproto_data() to get PDS endpoint and handle **Bug 2: DPoP Signature Format** - Fixed ES256 signature encoding from DER to IEEE P1363 format (dpop.py:_sign_jwt) - JWT signatures must use raw r|s concatenated (64 bytes), not DER encoding - Added decode_dss_signature to convert signature format - This was causing "invalid_dpop_proof" errors from auth servers **Bug 3: DPoP htu Field** - Fixed htu field to strip query and fragment per RFC 9449 (dpop.py:create_proof) - Was including full URL with query params, violating DPoP spec - Now properly parses URL and strips query/fragment **Added Tests:** - test_dpop_signature_format: Verifies 64-byte IEEE P1363 format - test_dpop_htu_strips_query_and_fragment: Tests URL stripping - test_dpop_jwk_format: Validates JWK structure in header All 15 tests now passing (was 12 before). Validated against official TypeScript SDK which had same htu bug. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
moved design docs and spec compliance to issue discussion. keeping implementation details in package README where they belong. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- add missing type annotations for route handlers - fix import ordering (flask imports after third-party) - use double quotes for multiline strings - add noqa comments for acceptable Exception catching in demo code - add noqa for debug=True (acceptable in demo context) - remove unused imports all ruff checks now pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- run ruff format on Flask demo app - generate sphinx documentation for atproto_oauth package - add atproto_oauth to docs module index resolves codegen_check and ruff CI failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
#6) per the ATProto OAuth spec, the aud claim in client assertion JWTs must be the Authorization Server's issuer URL, not the token endpoint URL. this was causing "unexpected aud claim value" errors when using confidential clients with plyr.fm. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <[email protected]>
the ATProto OAuth spec requires client assertions to include the kid (key ID) in the JWT header so the PDS knows which public key to use for verification. changes: - add client_secret_kid parameter to OAuthClient.__init__ - include kid in JWT header in _create_client_assertion() - add validation to require kid when using confidential client 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <[email protected]>
The PDS expands `include:namespace.permissionSet` scopes into `repo?collection=...` format. The previous strict string comparison failed when using permission sets. Added `_scopes_are_equivalent()` to semantically compare scopes, handling both direct `repo:` scopes and expanded permission sets. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
* feat: add prompt parameter to OAuth start_authorization Adds support for the OAuth `prompt` parameter in the authorization flow, enabling clients to control authorization server behavior: - `login`: Force re-authentication, ignoring remembered sessions - `select_account`: Show account selection UI instead of auto-selecting - `consent`: Force consent screen even if previously approved - `none`: Silent authentication (fails if interaction required) This is essential for multi-account experiences where users need to authenticate with a different account without the PDS auto-approving based on a remembered session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: extract PromptType constant and add unit tests - Extract inline Literal type to module-level PromptType constant - Export PromptType from atproto_oauth package - Add comprehensive unit tests for prompt parameter: - Test PromptType values and export - Test prompt is passed through to _send_par_request - Test prompt is included in PAR params when provided - Test prompt is omitted from PAR params when None 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
overview
this PR adds OAuth 2.1 support to the atproto Python SDK, implementing the ATProto OAuth specification. this enables secure, password-free authentication for ATProto applications.
architecture & design decisions
new package:
atproto_oauthcreated a standalone package following the SDK's existing modular structure:
why separate package? keeps OAuth concerns isolated, allows applications to use only what they need, and follows the pattern of
atproto_identity,atproto_crypto, etc.OAuth 2.1 compliance details
authorization code flow with PKCE
spec reference: RFC 7636, ATProto OAuth §3.2
implementation:
packages/atproto_oauth/pkce.py:25-43DPoP (demonstrating proof-of-possession)
spec reference: RFC 9449, ATProto OAuth §4.2
jtiper request (16-byte token)athclaim (access token hash) when presenting tokensimplementation:
packages/atproto_oauth/dpop.py:94-143nuance: DPoP nonces are tracked separately for auth server vs PDS, as they can issue different nonces. the client automatically handles nonce rotation when receiving
use_dpop_nonceerrors.pushed authorization requests (PAR)
spec reference: RFC 9126, ATProto OAuth §3.1.1
request_uriused in actual authorizationimplementation:
packages/atproto_oauth/client.py:204-241nuance: PAR endpoint validation is mandatory - we check
require_pushed_authorization_requests: truein server metadata (security.py:171-173).ATProto-specific implementation
DID-based identity resolution
spec reference: ATProto OAuth §2.2
the OAuth flow begins with a handle or DID, not a static service URL:
atproto_identity)implementation:
packages/atproto_oauth/client.py:76-95authorization server discovery
spec reference: ATProto OAuth §2.3
/.well-known/oauth-protected-resourcefrom PDSauthorization_serversarray/.well-known/oauth-authorization-serverfrom auth serverimplementation:
packages/atproto_oauth/metadata.py:10-131nuance: we validate the auth server URL matches the
issuerfield in metadata to prevent issuer confusion attacks (security.py:117-132).client identification
spec reference: ATProto OAuth §2.4
client_id_metadata_document_supported: trueimplementation: client metadata hosting is left to applications (varies by deployment). the SDK validates server support (
security.py:179-181).security features
SSRF protection
spec reference: ATProto OAuth Security Considerations
all URLs validated before fetching:
implementation:
packages/atproto_oauth/security.py:22-62state parameter for CSRF protection
implementation:
packages/atproto_oauth/client.py:159-167storage abstraction
pluggable stores
two abstract base classes for different storage needs:
StateStore- temporary OAuth state during authorization flow:SessionStore- long-lived OAuth sessions:implementation:
packages/atproto_oauth/stores/base.pybuilt-in memory stores provided for development. production use requires implementing persistent stores.
production case study: plyr.fm
plyr.fm uses this OAuth implementation in production with custom storage backends.
postgres state store
implements
StateStorewith postgres for multi-instance deployments:key decisions:
implementation: plyr.fm/backend/src/backend/_internal/oauth_stores/postgres.py
encrypted session storage
instead of using
SessionStoredirectly, plyr.fm stores OAuth sessions in postgres with encryption:approach:
OAuthSessionto JSON, then encryptsUserSessiontable with 2-week expirationwhy not use
SessionStore? needed tighter integration with app's session model (user profiles, artist status, etc.) and wanted control over encryption strategy.implementation: plyr.fm/backend/src/backend/_internal/auth.py:63-133
token refresh pattern
plyr.fm doesn't currently implement automatic token refresh, relying on the 2-week session expiration. when needed, refresh would follow this pattern:
testing
unit tests: 15 tests covering PKCE and DPoP functionality
uv run pytest tests/test_oauth_*.py -vflask demo: working example application
what's next (not in this PR)
this PR provides the core OAuth client. future enhancements could include:
atproto_client.Client- addlogin_oauth()methodsfeedback very welcome on the current implementation before exploring these additions.