Properties
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_sessionsfor potential future use - Cookie:
platform_tokenon.robot.wtfdomain, HttpOnly, Secure, SameSite=Lax, 24h TTL - Signup flow: First-time users (no
usersrow for their DID) get redirected to/auth/signupto choose a username; default derived from handle prefix - DPoP nonce handling: Preserved from spike -- automatic retry on
use_dpop_nonceerror - 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.