Commit f59029

2026-03-15 05:00:00 Claude (MCP): [mcp] VS-1: Document ATProto OAuth spike findings
/dev/null .. Dev/VS-1_ATProto_Spike.md
@@ 0,0 1,82 @@
+ ---
+ category: reference
+ tags: [spike, atproto, oauth, auth]
+ last_updated: 2026-03-15
+ confidence: high
+ ---
+
+ # VS-1: ATProto OAuth Spike
+
+ ## Status: COMPLETE — ready for manual deployment and testing
+
+ ## What was built
+
+ A throwaway spike adapting the [Bluesky cookbook Flask OAuth demo](https://github.com/bluesky-social/cookbook/tree/main/python-oauth-web-app) (CC-0) for robot.wtf identity-only authentication.
+
+ **Branch:** `feat/vs-1-atproto-spike` in the robot.wtf repo
+ **Location:** `spike/atproto-oauth/`
+
+ ## Key adaptations from cookbook demo
+
+ 1. **Scope changed to `"atproto"`** — identity-only, no PDS write access. The ATProto spec explicitly supports this for "Login with atproto" use cases.
+ 2. **Fixed client_id** at `https://robot.wtf/auth/client-metadata.json` (cookbook dynamically computes it from request host)
+ 3. **All routes under `/auth/`** prefix to match Caddy routing
+ 4. **JWK loaded from file** (`/srv/data/client_jwk.json`) instead of environment variable
+ 5. **Inline JWKS** in client metadata instead of separate `jwks_uri` endpoint (simpler for spike)
+ 6. **Profile page** displays DID, handle, and display name (fetched from public Bluesky API)
+ 7. **Removed** Bluesky posting feature, `atproto_util.py`, `bsky_util.py`
+ 8. **Removed** `regex` dependency — using stdlib `re` (no Unicode handle validation needed for spike)
+
+ ## Files
+
+ | File | Lines | Source |
+ |------|-------|--------|
+ | `app.py` | ~280 | Heavily adapted from cookbook `app.py` |
+ | `atproto_oauth.py` | ~230 | Cookbook, removed `pds_authed_req` |
+ | `atproto_identity.py` | ~100 | Cookbook, unchanged |
+ | `atproto_security.py` | ~40 | Cookbook, unchanged |
+ | `schema.sql` | ~20 | Cookbook, unchanged |
+ | `templates/` | 4 files | New (simpler than cookbook) |
+ | `requirements.txt` | 6 deps | Subset of cookbook |
+
+ ## Smoke test results
+
+ - Flask app starts and initializes SQLite database
+ - `/auth/client-metadata.json` serves correct JSON with all required ATProto OAuth fields
+ - `/auth/login` renders login form
+ - `/auth/` redirects to `/auth/login` when not authenticated
+ - Public key in JWKS contains no private material (`d` field absent)
+
+ ## Dependencies
+
+ ```
+ Flask>=3.0
+ authlib>=1.3
+ dnspython>=2.6
+ requests>=2.32
+ requests-hardened>=1.0.0b3
+ cryptography>=41.0
+ ```
+
+ ## Deployment steps
+
+ 1. Copy `spike/atproto-oauth/` to VPS at `/srv/app/atproto-spike/`
+ 2. Install deps in `/srv/app/venv`
+ 3. Set `FLASK_SECRET_KEY` env var
+ 4. Run `flask --app app run --host 127.0.0.1 --port 8003`
+ 5. Test at `https://robot.wtf/auth/login`
+
+ ## What to watch for during manual testing
+
+ - **DPoP nonce errors**: The spike preserves the cookbook's retry-on-nonce-error pattern. If the first PAR request fails with `use_dpop_nonce`, it retries with the server-provided nonce. Watch logs for `"retrying with new auth server DPoP nonce"`.
+ - **Client metadata fetch**: The PDS fetches `https://robot.wtf/auth/client-metadata.json` during PAR. If Caddy isn't routing correctly or the response is malformed, PAR will fail with a 400.
+ - **Scope verification**: The callback asserts `tokens["scope"] == "atproto"`. If the AS returns a different scope string, the assertion will fail. This is the most likely breakage point.
+ - **Session cookie**: Requires `FLASK_SECRET_KEY`. Without it, Flask will error on any session operation.
+
+ ## Findings for V3 production implementation
+
+ 1. The cookbook code is well-structured and directly usable. The `atproto_oauth.py` and `atproto_identity.py` modules can transfer to production with minimal changes.
+ 2. Identity-only scope (`"atproto"`) is confirmed supported by the spec. The `sub` field in the token response contains the DID.
+ 3. `requests-hardened` upgraded to 1.2.0 (stable, no longer beta). SSRF mitigations work.
+ 4. The `authlib.jose` module handles all JWT/JWK operations (no `joserfc` needed).
+ 5. For production: replace SQLite session store with the platform's user/session tables, mint platform JWT on successful auth, add error handling beyond bare `assert` statements.
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9