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_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.

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