Skip to content

Conversation

@krystofyah
Copy link

@krystofyah krystofyah commented Nov 30, 2025

Summary

An alternative approach to adding OAuth to the python client largely based on @zzstoatzz's https://github.com/zzstoatzz/atproto OAuth implementation fork but with a few main differences:

  1. uses an OAuthSession mixin to add oauth methods to the atproto_client.Client
  2. removes the stores from the Client. This approach requires storing sessions to be handled outside of the client
  3. currently does not have async methods
  4. mixin overwrites client's invoke method to make authenticated requests so that all namespace methods still work as they do for regular sessions. For example you can make calls like client.getPosts or client.app.bsky.feed.getFeedSkeleton` after establishing a session via oauth

Components

  • OauthSessionMethodsMixin - OAuth flow handling (PAR, token exchange, refresh)
  • DPoPManager - DPoP proof generation and nonce management
  • PKCEManager - PKCE challenge/verifier generation
  • OAuthSession - OAuth session state management

Usage

from atproto_client import Client

# 1. Create client with OAuth configuration
client = Client(
    client_id='https://myapp.example.com/client-metadata.json',
    redirect_uri='https://myapp.example.com/callback',
    scope='atproto',
)

# 2. Start authorization flow
auth_url, state, oauth_state = client.start_authorization('user.bsky.social')
# store oauth_state and redirect user to auth_url...

# 3. Handle callback (after user authorizes and is redirected back)
# callback params: ?code=xxx&state=xxx&iss=xxx

# get state from storage
oauth_state = ...
session = client.handle_callback(
    code=callback_params['code'],
    iss=callback_params['iss'],
    oauth_state=oauth_state,  # stored from step 2
)

# 4. Client is now authenticated - make API calls
profile = client.app.bsky.actor.get_profile({'actor': session.did})

# 5. Restore session later
client.import_oauth_session(session)

Testing

  • added tests for unit tests for PKCE, DPoP, and OAuth mixin
  • Manual testing with OAuth flow with django application

@krystofyah krystofyah marked this pull request as ready for review November 30, 2025 02:40
@krystofyah krystofyah marked this pull request as draft November 30, 2025 02:49
zzstoatzz added a commit to zzstoatzz/atproto that referenced this pull request Nov 30, 2025
incorporates the best aspects of PR MarshalX#640's mixin approach while keeping our
async support, security module, and comprehensive test coverage.

**key changes:**

- add OAuthSessionMixin and AsyncOAuthSessionMixin to atproto_client
- integrate mixins into Client and AsyncClient classes
- override _invoke to transparently handle OAuth sessions with DPoP
- add oauth_login(), oauth_logout(), export_oauth_session() methods
- update codegen to auto-transform sync mixin to async

**usage:**

```python
from atproto_client import Client
from atproto_oauth import OAuthClient

# get OAuth session via OAuthClient (unchanged)
oauth_client = OAuthClient(...)
session = await oauth_client.handle_callback(code, state, iss)

# use with Client - requests are now transparently authenticated
client = Client(
    oauth_client_id='https://myapp.com/client-metadata.json',
    oauth_redirect_uri='https://myapp.com/callback',
    oauth_scope='atproto',
)
client.oauth_login(session)

# all existing methods work with OAuth session
timeline = client.get_timeline()
```

**what this enables:**

- seamless integration: existing client methods work with OAuth sessions
- no manual DPoP proof generation for each request
- automatic nonce rotation on PDS requests
- session export/import for persistence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@krystofyah krystofyah marked this pull request as ready for review November 30, 2025 03:03
zzstoatzz added a commit to zzstoatzz/atproto that referenced this pull request Nov 30, 2025
incorporates the best aspects of PR MarshalX#640's mixin approach while keeping our
async support, security module, and comprehensive test coverage.

**key changes:**

- add OAuthSessionMixin and AsyncOAuthSessionMixin to atproto_client
- integrate mixins into Client and AsyncClient classes
- override _invoke to transparently handle OAuth sessions with DPoP
- add oauth_login(), oauth_logout(), export_oauth_session() methods
- update codegen to auto-transform sync mixin to async
- **remove make_authenticated_request()** - now handled by mixin's _invoke

**usage:**

```python
from atproto_client import AsyncClient
from atproto_oauth import OAuthClient

# get OAuth session via OAuthClient (unchanged)
oauth_client = OAuthClient(...)
session = await oauth_client.handle_callback(code, state, iss)

# use with Client - requests are now transparently authenticated
client = AsyncClient(
    oauth_client_id='https://myapp.com/client-metadata.json',
    oauth_redirect_uri='https://myapp.com/callback',
    oauth_scope='atproto',
)
client.oauth_login(session)

# all existing methods work with OAuth session
timeline = await client.get_timeline()
```

**what this enables:**

- seamless integration: existing client methods work with OAuth sessions
- no manual DPoP proof generation for each request
- automatic nonce rotation on PDS requests
- session export/import for persistence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
zzstoatzz added a commit to zzstoatzz/atproto that referenced this pull request Nov 30, 2025
incorporates the best aspects of PR MarshalX#640's mixin approach while keeping our
async support, security module, and comprehensive test coverage.

**key changes:**

- add OAuthSessionMixin and AsyncOAuthSessionMixin to atproto_client
- integrate mixins into Client and AsyncClient classes
- override _invoke to transparently handle OAuth sessions with DPoP
- add oauth_login(), oauth_logout(), export_oauth_session() methods
- update codegen to auto-transform sync mixin to async
- **remove make_authenticated_request()** - now handled by mixin's _invoke

**usage:**

```python
from atproto_client import AsyncClient
from atproto_oauth import OAuthClient

# get OAuth session via OAuthClient (unchanged)
oauth_client = OAuthClient(...)
session = await oauth_client.handle_callback(code, state, iss)

# use with Client - requests are now transparently authenticated
client = AsyncClient(
    oauth_client_id='https://myapp.com/client-metadata.json',
    oauth_redirect_uri='https://myapp.com/callback',
    oauth_scope='atproto',
)
client.oauth_login(session)

# all existing methods work with OAuth session
timeline = await client.get_timeline()
```

**what this enables:**

- seamless integration: existing client methods work with OAuth sessions
- no manual DPoP proof generation for each request
- automatic nonce rotation on PDS requests
- session export/import for persistence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
zzstoatzz added a commit to zzstoatzz/atproto that referenced this pull request Nov 30, 2025
incorporates the best aspects of PR MarshalX#640's mixin approach while keeping our
async support, security module, and comprehensive test coverage.

**key changes:**

- add OAuthSessionMixin and AsyncOAuthSessionMixin to atproto_client
- integrate mixins into Client and AsyncClient classes
- override _invoke to transparently handle OAuth sessions with DPoP
- add oauth_login(), oauth_logout(), export_oauth_session() methods
- update codegen to auto-transform sync mixin to async
- **remove make_authenticated_request()** - now handled by mixin's _invoke
- **remove SessionStore** - caller now manages session persistence
- **remove MemorySessionStore** - no longer needed
- simplify OAuthClient to only require StateStore

**usage:**

```python
from atproto_client import AsyncClient
from atproto_oauth import OAuthClient
from atproto_oauth.stores import MemoryStateStore

# OAuth client only needs state store now
oauth = OAuthClient(
    client_id='https://myapp.com/client-metadata.json',
    redirect_uri='https://myapp.com/callback',
    scope='atproto',
    state_store=MemoryStateStore(),
)

# get session - caller persists it however they want
session = await oauth.handle_callback(code, state, iss)
await save_to_database(session)  # your responsibility

# use with Client
client = AsyncClient(
    oauth_client_id='https://myapp.com/client-metadata.json',
    oauth_redirect_uri='https://myapp.com/callback',
    oauth_scope='atproto',
)
client.oauth_login(session)

# all existing methods work with OAuth session
timeline = await client.get_timeline()
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@MarshalX
Copy link
Owner

MarshalX commented Dec 8, 2025

Hey @zzstoatzz and @krystofyah! Thank you for working on it. Could you please help me to understand on which PR should I look for as the main one?

obraz

Also, I do see in attached PR by @zzstoatzz that mixin approach does not fit well into 3d parties projects.

Having dpop.py and pkce.py in method_mixins folder is a bit strange to me. Since we do have atproto_crypto package. Also, I do see that official SDK separated OAuth into isolated packages. Which I could see in @zzstoatzz pull request (atproto_oauth)

@zzstoatzz
Copy link
Contributor

zzstoatzz commented Dec 8, 2025

in my attempted adoption of the mixin pattern it appeared to cause complexity, iirc, it seemed like the mixin pattern might be more complex for per-request client situations as opposed to single clients. to be fair that could have been for reasons idiosyncratic to the use case. will do a little more digging and clean up my PR when time allows, happy to coordinate / merge efforts with @krystofyah if that works out!

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 this pull request may close these issues.

3 participants