# auth atproto uses OAuth 2.0 for application authorization. ## the flow 1. user visits application 2. application redirects to user's PDS for authorization 3. user approves requested scopes 4. PDS redirects back with authorization code 5. application exchanges code for tokens 6. application uses tokens to act on user's behalf standard OAuth, but the authorization server is the user's PDS, not a central service. ## scopes scopes define what an application can do: ``` atproto # full access (legacy) repo:fm.plyr.track # read/write fm.plyr.track collection repo:fm.plyr.like # read/write fm.plyr.like collection repo:read # read-only access to repo ``` granular scopes let users grant minimal permissions. an app that only needs to read your profile shouldn't have write access to your posts. ## permission sets listing individual scopes is noisy. permission sets bundle them under human-readable names: ``` include:fm.plyr.authFullApp # "plyr.fm Music Library" ``` instead of seeing `fm.plyr.track, fm.plyr.like, fm.plyr.comment, ...`, users see a single permission with a description. permission sets are lexicons published to `com.atproto.lexicon.schema` on your authority repo. from [plyr.fm permission sets](https://github.com/zzstoatzz/plyr.fm/blob/main/docs/lexicons/overview.md#permission-sets) ## session management tokens expire. applications need refresh logic: ```python class SessionManager: def __init__(self, session_path: Path): self.session_path = session_path self._client: AsyncClient | None = None async def get_client(self) -> AsyncClient: if self._client: return self._client # try loading saved session if self.session_path.exists(): session_str = self.session_path.read_text() self._client = AsyncClient() await self._client.login(session_string=session_str) self._client.on_session_change(self._save_session) return self._client # fall back to fresh login self._client = AsyncClient() await self._client.login(handle, password) self._save_session(None, None) return self._client def _save_session(self, event, session): self.session_path.write_text(self._client.export_session_string()) ``` from [bot](https://github.com/zzstoatzz/bot) - persists sessions to disk, refreshes automatically. ## per-request credentials for multi-tenant applications (one backend serving many users), credentials come per-request: ```python # middleware extracts from headers x-atproto-handle: user.handle x-atproto-password: app-password # or from OAuth session authorization: Bearer ``` from [pdsx MCP server](https://github.com/zzstoatzz/pdsx) - accepts credentials via HTTP headers for multi-tenant deployment. ## app passwords for bots and automated tools, app passwords are simpler than full OAuth: 1. user creates app password in their PDS settings 2. bot uses handle + app password to authenticate 3. no redirect flow needed app passwords have full account access. use OAuth with scopes when you need granular permissions. ## why this matters OAuth at the protocol level means: - users authorize apps, not the other way around - applications can't lock in users by controlling auth - the same identity works across all atmospheric applications - granular scopes enable minimal-permission applications