Commit 261081
2026-03-15 06:17:07 Claude (MCP): [mcp] V3 ATProto Auth implementation summary| /dev/null .. Dev/V3_ATProto_Auth_Summary.md | |
| @@ 0,0 1,64 @@ | |
| + | --- |
| + | category: reference |
| + | tags: |
| + | - v3 |
| + | - auth |
| + | - atproto |
| + | - oauth |
| + | last_updated: 2026-03-15 |
| + | --- |
| + | |
| + | # V3: ATProto OAuth Production Auth Service |
| + | |
| + | **Branch:** `feat/v3-atproto-auth` |
| + | **Commit:** `4580bbf` |
| + | **Status:** Implementation complete, 24/24 tests pass (79/79 total) |
| + | |
| + | ## What Changed |
| + | |
| + | Replaced the stub auth server (`app/auth_server.py`) with a production ATProto OAuth flow adapted from the VS-1 spike (`spike/atproto-oauth/`). |
| + | |
| + | ## Deliverables |
| + | |
| + | | File | Description | |
| + | |------|-------------| |
| + | | `app/auth/atproto_oauth.py` | PAR, DPoP, token exchange, refresh, revocation | |
| + | | `app/auth/atproto_identity.py` | Handle/DID resolution (DNS TXT + HTTP well-known) | |
| + | | `app/auth/atproto_security.py` | SSRF mitigations, hardened HTTP client | |
| + | | `app/auth_server.py` | Full Flask app with `create_app()` factory | |
| + | | `app/auth/templates/` | login.html, signup.html, error.html, base.html (Pico CSS) | |
| + | | `tests/test_auth_server.py` | 24 tests covering all routes and flows | |
| + | | `requirements.txt` | Added authlib>=1.3, dnspython>=2.6, requests-hardened>=1.0.0b3 | |
| + | | `ansible/roles/database/files/schema.sql` | Added `oauth_auth_requests` table, updated `oauth_sessions` | |
| + | | `ansible/roles/deploy/files/robot-auth.service` | Added EnvironmentFile, CLIENT_JWK_PATH, ROBOT_DB_PATH | |
| + | |
| + | ## Routes |
| + | |
| + | | Method | Path | Purpose | |
| + | |--------|------|---------| |
| + | | GET | `/auth/client-metadata.json` | ATProto client metadata (client_id URL) | |
| + | | GET | `/auth/login` | Login page with handle input | |
| + | | POST | `/auth/login` | Initiate OAuth: resolve identity, PAR, redirect to AS | |
| + | | GET | `/auth/callback` | Exchange code for tokens, issue platform JWT cookie | |
| + | | GET | `/auth/signup` | Username form (first-time users) | |
| + | | POST | `/auth/signup` | Create user record, issue JWT cookie | |
| + | | GET | `/auth/logout` | Revoke ATProto tokens, clear cookie | |
| + | | GET | `/.well-known/oauth-authorization-server` | AS metadata stub | |
| + | | GET | `/.well-known/jwks.json` | RS256 public key | |
| + | |
| + | ## Key Design Decisions |
| + | |
| + | - **Platform JWT != ATProto tokens**: OAuth callback issues an RS256 platform JWT cookie; ATProto tokens are stored in `oauth_sessions` for potential future use |
| + | - **Cookie**: `platform_token` on `.robot.wtf` domain, HttpOnly, Secure, SameSite=Lax, 24h TTL |
| + | - **Signup flow**: First-time users (no `users` row for their DID) get redirected to `/auth/signup` to choose a username; default derived from handle prefix |
| + | - **DPoP nonce handling**: Preserved from spike -- automatic retry on `use_dpop_nonce` error |
| + | - **client_id**: `https://robot.wtf/auth/client-metadata.json` (stable URL) |
| + | - **Scope**: `atproto` (identity-only, no repo access) |
| + | |
| + | ## Schema Changes |
| + | |
| + | Added `oauth_auth_requests` table for in-flight OAuth state (temporary, deleted after callback). Updated `oauth_sessions` to use DID as primary key with ATProto-specific fields (authserver_iss, pds_url, dpop_authserver_nonce, dpop_private_jwk). |
| + | |
| + | ## Test Coverage |
| + | |
| + | 24 tests covering: client metadata structure, login page rendering, invalid input handling, OAuth callback (error, missing params, unknown state, returning user, new user), signup flow (no session, form rendering, user creation, invalid/reserved/duplicate username rejection), logout cookie clearing, JWKS endpoint, AS metadata, and default username derivation. |