Blame
|
1 | --- |
||||||
| 2 | category: reference |
|||||||
| 3 | tags: |
|||||||
| 4 | - v3 |
|||||||
| 5 | - auth |
|||||||
| 6 | - atproto |
|||||||
| 7 | - oauth |
|||||||
| 8 | last_updated: 2026-03-15 |
|||||||
| 9 | --- |
|||||||
| 10 | ||||||||
| 11 | # V3: ATProto OAuth Production Auth Service |
|||||||
| 12 | ||||||||
| 13 | **Branch:** `feat/v3-atproto-auth` |
|||||||
| 14 | **Commit:** `4580bbf` |
|||||||
| 15 | **Status:** Implementation complete, 24/24 tests pass (79/79 total) |
|||||||
| 16 | ||||||||
| 17 | ## What Changed |
|||||||
| 18 | ||||||||
| 19 | Replaced the stub auth server (`app/auth_server.py`) with a production ATProto OAuth flow adapted from the VS-1 spike (`spike/atproto-oauth/`). |
|||||||
| 20 | ||||||||
| 21 | ## Deliverables |
|||||||
| 22 | ||||||||
| 23 | | File | Description | |
|||||||
| 24 | |------|-------------| |
|||||||
| 25 | | `app/auth/atproto_oauth.py` | PAR, DPoP, token exchange, refresh, revocation | |
|||||||
| 26 | | `app/auth/atproto_identity.py` | Handle/DID resolution (DNS TXT + HTTP well-known) | |
|||||||
| 27 | | `app/auth/atproto_security.py` | SSRF mitigations, hardened HTTP client | |
|||||||
| 28 | | `app/auth_server.py` | Full Flask app with `create_app()` factory | |
|||||||
| 29 | | `app/auth/templates/` | login.html, signup.html, error.html, base.html (Pico CSS) | |
|||||||
| 30 | | `tests/test_auth_server.py` | 24 tests covering all routes and flows | |
|||||||
| 31 | | `requirements.txt` | Added authlib>=1.3, dnspython>=2.6, requests-hardened>=1.0.0b3 | |
|||||||
| 32 | | `ansible/roles/database/files/schema.sql` | Added `oauth_auth_requests` table, updated `oauth_sessions` | |
|||||||
| 33 | | `ansible/roles/deploy/files/robot-auth.service` | Added EnvironmentFile, CLIENT_JWK_PATH, ROBOT_DB_PATH | |
|||||||
| 34 | ||||||||
| 35 | ## Routes |
|||||||
| 36 | ||||||||
| 37 | | Method | Path | Purpose | |
|||||||
| 38 | |--------|------|---------| |
|||||||
| 39 | | GET | `/auth/client-metadata.json` | ATProto client metadata (client_id URL) | |
|||||||
| 40 | | GET | `/auth/login` | Login page with handle input | |
|||||||
| 41 | | POST | `/auth/login` | Initiate OAuth: resolve identity, PAR, redirect to AS | |
|||||||
| 42 | | GET | `/auth/callback` | Exchange code for tokens, issue platform JWT cookie | |
|||||||
| 43 | | GET | `/auth/signup` | Username form (first-time users) | |
|||||||
| 44 | | POST | `/auth/signup` | Create user record, issue JWT cookie | |
|||||||
| 45 | | GET | `/auth/logout` | Revoke ATProto tokens, clear cookie | |
|||||||
| 46 | | GET | `/.well-known/oauth-authorization-server` | AS metadata stub | |
|||||||
| 47 | | GET | `/.well-known/jwks.json` | RS256 public key | |
|||||||
| 48 | ||||||||
| 49 | ## Key Design Decisions |
|||||||
| 50 | ||||||||
| 51 | - **Platform JWT != ATProto tokens**: OAuth callback issues an RS256 platform JWT cookie; ATProto tokens are stored in `oauth_sessions` for potential future use |
|||||||
| 52 | - **Cookie**: `platform_token` on `.robot.wtf` domain, HttpOnly, Secure, SameSite=Lax, 24h TTL |
|||||||
| 53 | - **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 |
|||||||
| 54 | - **DPoP nonce handling**: Preserved from spike -- automatic retry on `use_dpop_nonce` error |
|||||||
| 55 | - **client_id**: `https://robot.wtf/auth/client-metadata.json` (stable URL) |
|||||||
| 56 | - **Scope**: `atproto` (identity-only, no repo access) |
|||||||
| 57 | ||||||||
| 58 | ## Schema Changes |
|||||||
| 59 | ||||||||
| 60 | 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). |
|||||||
| 61 | ||||||||
| 62 | ## Test Coverage |
|||||||
| 63 | ||||||||
| 64 | 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. |
|||||||