A community based topic aggregation platform built on atproto
1# atProto OAuth Authentication 2 3This package implements third-party OAuth authentication for Coves, validating JWT Bearer tokens from mobile apps and other atProto clients. 4 5## Architecture 6 7This is **third-party authentication** (validating incoming requests), not first-party authentication (logging users into Coves web frontend). 8 9### Components 10 111. **JWT Parser** (`jwt.go`) - Parses and validates JWT tokens 122. **JWKS Fetcher** (`jwks_fetcher.go`) - Fetches and caches public keys from PDS authorization servers 133. **Auth Middleware** (`internal/api/middleware/auth.go`) - HTTP middleware that protects endpoints 14 15### Flow 16 17``` 18Client Request 19 20Authorization: Bearer <jwt> 21 22Auth Middleware 23 24Extract JWT → Parse Claims → Verify Signature (via JWKS) 25 26Inject DID into Context → Call Handler 27``` 28 29## Usage 30 31### Phase 1: Parse-Only Mode (Testing) 32 33Set `AUTH_SKIP_VERIFY=true` to only parse JWTs without signature verification: 34 35```bash 36export AUTH_SKIP_VERIFY=true 37``` 38 39This is useful for: 40- Initial integration testing 41- Testing with mock tokens 42- Debugging JWT structure 43 44### Phase 2: Full Verification (Production) 45 46Set `AUTH_SKIP_VERIFY=false` (or unset) to enable full JWT signature verification: 47 48```bash 49export AUTH_SKIP_VERIFY=false 50# or just unset it 51``` 52 53This is **required for production** and validates: 54- JWT signature using PDS public key 55- Token expiration 56- Required claims (sub, iss) 57- DID format 58 59## Protected Endpoints 60 61The following endpoints require authentication: 62 63- `POST /xrpc/social.coves.community.create` 64- `POST /xrpc/social.coves.community.update` 65- `POST /xrpc/social.coves.community.subscribe` 66- `POST /xrpc/social.coves.community.unsubscribe` 67 68### Making Authenticated Requests 69 70Include the JWT in the `Authorization` header: 71 72```bash 73curl -X POST https://coves.social/xrpc/social.coves.community.create \ 74 -H "Authorization: Bearer eyJhbGc..." \ 75 -H "Content-Type: application/json" \ 76 -d '{"name":"Gaming","hostedByDid":"did:plc:..."}' 77``` 78 79### Getting User DID in Handlers 80 81The middleware injects the authenticated user's DID into the request context: 82 83```go 84import "Coves/internal/api/middleware" 85 86func (h *Handler) HandleCreate(w http.ResponseWriter, r *http.Request) { 87 // Extract authenticated user DID 88 userDID := middleware.GetUserDID(r) 89 if userDID == "" { 90 // Not authenticated (should never happen with RequireAuth middleware) 91 http.Error(w, "Unauthorized", http.StatusUnauthorized) 92 return 93 } 94 95 // Use userDID for authorization checks 96 // ... 97} 98``` 99 100## Key Caching 101 102Public keys are fetched from PDS authorization servers and cached for 1 hour. The cache is automatically cleaned up hourly to remove expired entries. 103 104### JWKS Discovery Flow 105 1061. Extract `iss` claim from JWT (e.g., `https://pds.example.com`) 1072. Fetch `https://pds.example.com/.well-known/oauth-authorization-server` 1083. Extract `jwks_uri` from metadata 1094. Fetch JWKS from `jwks_uri` 1105. Find matching key by `kid` from JWT header 1116. Cache the JWKS for 1 hour 112 113## DPoP Token Binding 114 115DPoP (Demonstrating Proof-of-Possession) binds access tokens to client-controlled cryptographic keys, preventing token theft and replay attacks. 116 117### What is DPoP? 118 119DPoP is an OAuth extension (RFC 9449) that adds proof-of-possession semantics to bearer tokens. When a PDS issues a DPoP-bound access token: 120 1211. Access token contains `cnf.jkt` claim (JWK thumbprint of client's public key) 1222. Client creates a DPoP proof JWT signed with their private key 1233. Server verifies the proof signature and checks it matches the token's `cnf.jkt` 124 125### CRITICAL: DPoP Security Model 126 127> ⚠️ **DPoP is an ADDITIONAL security layer, NOT a replacement for token signature verification.** 128 129The correct verification order is: 1301. **ALWAYS verify the access token signature first** (via JWKS, HS256 shared secret, or DID resolution) 1312. **If the verified token has `cnf.jkt`, REQUIRE valid DPoP proof** 1323. **NEVER use DPoP as a fallback when signature verification fails** 133 134**Why This Matters**: An attacker could create a fake token with `sub: "did:plc:victim"` and their own `cnf.jkt`, then present a valid DPoP proof signed with their key. If we accept DPoP as a fallback, the attacker can impersonate any user. 135 136### How DPoP Works 137 138``` 139┌─────────────┐ ┌─────────────┐ 140│ Client │ │ Server │ 141│ │ │ (Coves) │ 142└─────────────┘ └─────────────┘ 143 │ │ 144 │ 1. Authorization: Bearer <token> │ 145 │ DPoP: <proof-jwt> │ 146 │───────────────────────────────────────>│ 147 │ │ 148 │ │ 2. VERIFY token signature 149 │ │ (REQUIRED - no fallback!) 150 │ │ 151 │ │ 3. If token has cnf.jkt: 152 │ │ - Verify DPoP proof 153 │ │ - Check thumbprint match 154 │ │ 155 │ 200 OK │ 156 │<───────────────────────────────────────│ 157``` 158 159### When DPoP is Required 160 161DPoP verification is **REQUIRED** when: 162- Access token signature has been verified AND 163- Access token contains `cnf.jkt` claim (DPoP-bound) 164 165If the token has `cnf.jkt` but no DPoP header is present, the request is **REJECTED**. 166 167### Replay Protection 168 169DPoP proofs include a unique `jti` (JWT ID) claim. The server tracks seen `jti` values to prevent replay attacks: 170 171```go 172// Create a verifier with replay protection (default) 173verifier := auth.NewDPoPVerifier() 174defer verifier.Stop() // Stop cleanup goroutine on shutdown 175 176// The verifier automatically rejects reused jti values within the proof validity window (5 minutes) 177``` 178 179### DPoP Implementation 180 181The `dpop.go` module provides: 182 183```go 184// Create a verifier with replay protection 185verifier := auth.NewDPoPVerifier() 186defer verifier.Stop() 187 188// Verify the DPoP proof 189proof, err := verifier.VerifyDPoPProof(dpopHeader, "POST", "https://coves.social/xrpc/...") 190if err != nil { 191 // Invalid proof (includes replay detection) 192} 193 194// Verify it binds to the VERIFIED access token 195expectedThumbprint, err := auth.ExtractCnfJkt(claims) 196if err != nil { 197 // Token not DPoP-bound 198} 199 200if err := verifier.VerifyTokenBinding(proof, expectedThumbprint); err != nil { 201 // Proof doesn't match token 202} 203``` 204 205### DPoP Proof Format 206 207The DPoP header contains a JWT with: 208 209**Header**: 210- `typ`: `"dpop+jwt"` (required) 211- `alg`: `"ES256"` (or other supported algorithm) 212- `jwk`: Client's public key (JWK format) 213 214**Claims**: 215- `jti`: Unique proof identifier (tracked for replay protection) 216- `htm`: HTTP method (e.g., `"POST"`) 217- `htu`: HTTP URI (without query/fragment) 218- `iat`: Timestamp (must be recent, within 5 minutes) 219 220**Example**: 221```json 222{ 223 "typ": "dpop+jwt", 224 "alg": "ES256", 225 "jwk": { 226 "kty": "EC", 227 "crv": "P-256", 228 "x": "...", 229 "y": "..." 230 } 231} 232{ 233 "jti": "unique-id-123", 234 "htm": "POST", 235 "htu": "https://coves.social/xrpc/social.coves.community.create", 236 "iat": 1700000000 237} 238``` 239 240## Security Considerations 241 242### ✅ Implemented 243 244- JWT signature verification with PDS public keys 245- Token expiration validation 246- DID format validation 247- Required claims validation (sub, iss) 248- Key caching with TTL 249- Secure error messages (no internal details leaked) 250- **DPoP proof verification** (proof-of-possession for token binding) 251- **DPoP thumbprint validation** (prevents token theft attacks) 252- **DPoP freshness checks** (5-minute proof validity window) 253- **DPoP replay protection** (jti tracking with in-memory cache) 254- **Secure DPoP model** (DPoP required AFTER signature verification, never as fallback) 255 256### ⚠️ Not Yet Implemented 257 258- Server-issued DPoP nonces (additional replay protection) 259- Scope validation (checking `scope` claim) 260- Audience validation (checking `aud` claim) 261- Rate limiting per DID 262- Token revocation checking 263 264## Testing 265 266Run the test suite: 267 268```bash 269go test ./internal/atproto/auth/... -v 270``` 271 272### Manual Testing 273 2741. **Phase 1 (Parse Only)**: 275 ```bash 276 # Create a test JWT (use jwt.io or a tool) 277 export AUTH_SKIP_VERIFY=true 278 curl -X POST http://localhost:8081/xrpc/social.coves.community.create \ 279 -H "Authorization: Bearer <test-jwt>" \ 280 -d '{"name":"Test","hostedByDid":"did:plc:test"}' 281 ``` 282 2832. **Phase 2 (Full Verification)**: 284 ```bash 285 # Use a real JWT from a PDS 286 export AUTH_SKIP_VERIFY=false 287 curl -X POST http://localhost:8081/xrpc/social.coves.community.create \ 288 -H "Authorization: Bearer <real-jwt>" \ 289 -d '{"name":"Test","hostedByDid":"did:plc:test"}' 290 ``` 291 292## Error Responses 293 294### 401 Unauthorized 295 296Missing or invalid token: 297 298```json 299{ 300 "error": "AuthenticationRequired", 301 "message": "Missing Authorization header" 302} 303``` 304 305```json 306{ 307 "error": "AuthenticationRequired", 308 "message": "Invalid or expired token" 309} 310``` 311 312### Common Issues 313 3141. **Missing Authorization header** → Add `Authorization: Bearer <token>` 3152. **Token expired** → Get a new token from PDS 3163. **Invalid signature** → Ensure token is from a valid PDS 3174. **JWKS fetch fails** → Check PDS availability and network connectivity 318 319## Future Enhancements 320 321- [ ] DPoP nonce validation (server-managed nonce for additional replay protection) 322- [ ] Scope-based authorization 323- [ ] Audience claim validation 324- [ ] Token revocation support 325- [ ] Rate limiting per DID 326- [ ] Metrics and monitoring