# OAuth Authentication PRD: Third-Party Client Support ## ✅ Implementation Status **Phase 1 & 2: COMPLETED** (2025-10-16) - ✅ JWT parsing and validation implemented - ✅ JWT signature verification with PDS public keys (RSA + ECDSA/ES256) - ✅ JWKS fetching and caching (1 hour TTL) - ✅ Auth middleware protecting community endpoints - ✅ Handlers updated to use `GetUserDID(r)` - ✅ Comprehensive middleware auth tests (11 test cases) - ✅ E2E tests updated to use Bearer tokens - ✅ Security logging with IP, method, path, issuer - ✅ Scope validation (atproto required) - ✅ Issuer HTTPS validation - ✅ CreatedByDID validation in handlers - ✅ All tests passing - ✅ Documentation complete **Implementation Location**: `internal/atproto/auth/`, `internal/api/middleware/auth.go` **Configuration**: Set `AUTH_SKIP_VERIFY=false` for full signature verification (recommended for production). **Security Notes**: - Phase 1 (skipVerify=true): Parses and validates JWT claims without signature verification - suitable for alpha with trusted users - Phase 2 (skipVerify=false): Full cryptographic signature verification with PDS public keys - production-ready **Next Steps**: Phase 3 (DPoP validation, audience validation, JWKS fetcher tests) can be implemented when needed for production hardening. --- ## Overview Coves needs to validate OAuth tokens from third-party atProto clients to enable authenticated API access. This is critical for the community endpoints (create, update, subscribe, unsubscribe) which currently use an insecure placeholder (`X-User-DID` header). ## Why This Is Needed for Coves ### The Problem Currently, Coves community endpoints accept an `X-User-DID` header that **anyone can forge**. This is fundamentally insecure and allows: - Impersonation attacks (claiming to be any DID) - Unauthorized community creation - Fake subscriptions - Malicious updates to communities Example of current vulnerability: ```bash # Anyone can pretend to be alice by setting a header curl -X POST https://coves.social/xrpc/social.coves.community.create \ -H "X-User-DID: did:plc:alice123" \ -d '{"name": "fake-community", ...}' ``` ### Why Third-Party OAuth? Unlike traditional APIs where you control the auth flow, **atProto is federated**: 1. **Users authenticate with their PDS**, not with Coves 2. **Third-party apps** (mobile apps, desktop clients, browser extensions) obtain tokens from the user's PDS 3. **Coves must validate** these tokens when clients make requests on behalf of users This is fundamentally different from traditional OAuth where you're the authorization server. In atProto: - **You are NOT the auth server** - Each PDS is its own authorization server - **You are a resource server** - You validate tokens issued by arbitrary PDSes - **You cannot control token issuance** - Only validation ### Why This Differs From First-Party OAuth Coves has two separate OAuth systems that serve different purposes: | System | Purpose | Location | Token Source | |--------|---------|----------|--------------| | **First-Party OAuth** | Authenticate users for Coves web UI | `internal/core/oauth/` | Coves issues tokens | | **Third-Party OAuth** | Validate tokens from external apps | *To be implemented* | User's PDS issues tokens | **First-party OAuth** is for if/when you build a Coves web frontend. It implements the **client side** of OAuth (login flows, token refresh, etc.). **Third-party OAuth validation** is for the **server side** - validating incoming tokens from arbitrary clients you didn't build. ## Current State ### Existing Infrastructure #### 1. First-Party OAuth (Client-Side) - **Location**: `internal/core/oauth/`, `internal/api/handlers/oauth/` - **Purpose**: For a potential Coves web frontend - **What it does**: - Login flows (`/oauth/login`, `/oauth/callback`) - Session management (cookie + database) - Token refresh - Client metadata (`/oauth/client-metadata.json`) - **What it does NOT do**: Validate incoming tokens from third-party apps #### 2. Placeholder Auth (INSECURE) - **Location**: Community handlers (`internal/api/handlers/community/*.go`) - **Current implementation**: ```go // INSECURE - allows impersonation userDID := r.Header.Get("X-User-DID") ``` - **Used by**: - `POST /xrpc/social.coves.community.create` - `POST /xrpc/social.coves.community.update` - `POST /xrpc/social.coves.community.subscribe` - `POST /xrpc/social.coves.community.unsubscribe` ### Protected vs Public Endpoints #### ✅ Public Endpoints (No auth required) These are read-only endpoints that anyone can access: - `GET /xrpc/social.coves.community.get` - View a community - `GET /xrpc/social.coves.community.list` - List communities - `GET /xrpc/social.coves.community.search` - Search communities **Rationale**: Public discovery is essential for network effects and user experience. #### 🔒 Protected Endpoints (Require authentication) These modify state and must verify the user's identity: - `POST /xrpc/social.coves.community.create` - Creates a community owned by the authenticated user - `POST /xrpc/social.coves.community.update` - Updates a community (must be owner/moderator) - `POST /xrpc/social.coves.community.subscribe` - Creates a subscription record in the user's repo - `POST /xrpc/social.coves.community.unsubscribe` - Deletes a subscription from the user's repo **Rationale**: These operations write to the user's repository or create resources owned by the user or the Coves instance (Communities), so we must cryptographically verify their identity. ## atProto OAuth Requirements ### How Third-Party Clients Work When a third-party app (e.g., a mobile client for Coves) wants to make authenticated requests: ``` ┌─────────────┐ ┌─────────────┐ │ User's │ │ User's │ │ Mobile App │ │ PDS │ │ │ 1. Initiate OAuth flow │ │ │ │──────────────────────────────>│ │ │ │ │ │ │ │ 2. User authorizes │ │ │ │ 3. Receive access token │ │ │ │<──────────────────────────────│ │ └─────────────┘ └─────────────┘ │ │ 4. Make authenticated request │ Authorization: DPoP │ DPoP: ↓ ┌─────────────┐ │ Coves │ │ AppView │ 5. Validate token & DPoP │ │ 6. Extract user DID │ │ 7. Process request └─────────────┘ ``` ### Token Format Third-party clients send two headers: **1. Authorization Header** ``` Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImRpZDpwbGM6YWxpY2UjYXRwcm90by1wZHMifQ... ``` Format: `DPoP ` The access token is a JWT containing: ```json { "iss": "https://user-pds.example.com", // PDS that issued token "sub": "did:plc:alice123", // User's DID "aud": "https://coves.social", // Target resource server (optional) "scope": "atproto", // Required scope "exp": 1698765432, // Expiration timestamp "iat": 1698761832, // Issued at timestamp "jti": "unique-token-id", // Unique token identifier "cnf": { // Confirmation claim (DPoP binding) "jkt": "hash-of-dpop-public-key" } } ``` **2. DPoP Header** ``` DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0... ``` The DPoP proof is a JWT proving possession of the private key bound to the access token: ```json { "typ": "dpop+jwt", "alg": "ES256", "jwk": { // Public key (ephemeral) "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } } // Payload: { "jti": "unique-proof-id", // Unique proof identifier "htm": "POST", // HTTP method "htu": "https://coves.social/xrpc/...", // Target URL (without query params) "iat": 1698761832, // Issued at "ath": "hash-of-access-token", // Hash of access token (SHA-256) "nonce": "server-provided-nonce" // Server nonce (after first request) } ``` ### Validation Requirements To properly validate incoming requests, Coves must: #### 1. Extract and Parse Tokens - Extract `Authorization: DPoP ` header - Extract `DPoP: ` header - Parse both as JWTs #### 2. Validate Access Token Structure - Check token is a valid JWT - Verify required claims exist (`iss`, `sub`, `exp`, `scope`, `cnf`) - Check `scope` includes `atproto` - Check `exp` hasn't passed #### 3. Fetch PDS Public Keys - Extract PDS URL from `iss` claim - Fetch `/.well-known/oauth-authorization-server` metadata - Get `jwks_uri` from metadata - Fetch public keys from `jwks_uri` - **Cache keys with appropriate TTL** (critical for performance) #### 4. Verify Access Token Signature - Find correct public key (match `kid` from JWT header) - Verify JWT signature using PDS public key - Cryptographically proves token was issued by claimed PDS #### 5. Validate DPoP Proof - Parse DPoP JWT - Verify DPoP signature using public key in `jwk` claim - Check `htm` matches request HTTP method - Check `htu` matches request URL (without query params) - Check `ath` matches hash of access token - Verify `jkt` in access token matches hash of DPoP public key - Check `iat` is recent (prevent replay attacks) #### 6. Handle Nonces (Replay Prevention) - First request: no nonce required - Return `DPoP-Nonce` header in response - Subsequent requests: verify nonce in DPoP proof - Rotate nonces periodically ## Why Alternative Solutions Aren't Feasible ### Option 1: Use Indigo's OAuth Package ❌ **What we investigated**: `github.com/bluesky-social/indigo/atproto/auth/oauth` **Why it doesn't work**: - Indigo's OAuth package is **client-side only** - Designed for apps that **make requests**, not **receive requests** - No token validation for resource servers - No DPoP proof verification utilities **What it provides**: ```go // Client-side only: - ClientApp.StartAuthFlow() // Initiate login - ClientApp.ProcessCallback() // Handle OAuth callback - ClientSession.RefreshToken() // Refresh tokens ``` **What it does NOT provide**: ```go // Server-side validation (missing): - ValidateAccessToken() // ❌ Not available - ValidateDPoPProof() // ❌ Not available - FetchPDSKeys() // ❌ Not available ``` ### Option 2: Use Indigo's Service Auth ❌ **What we investigated**: `github.com/bluesky-social/indigo/atproto/auth.ServiceAuthValidator` **Why it doesn't work**: - Service auth is for **service-to-service** communication, not user auth - Different token format (short-lived JWTs, 60s TTL) - Different validation logic (no DPoP, different audience) - Used when PDS calls AppView **on behalf of user**, not when **user's app** calls AppView **Service Auth vs User OAuth**: ``` Service Auth: User OAuth: PDS → AppView Third-party App → AppView Short-lived (60s) Long-lived (hours) No DPoP DPoP required Service DID User DID ``` ### Option 3: Use Tangled's Implementation ❌ **What we investigated**: Tangled's codebase at `/home/bretton/Code/tangled/core` **Why it doesn't work**: - Tangled uses **first-party OAuth only** (their own web UI) - No third-party token validation implemented - Uses same indigo service auth we already ruled out - Custom `icyphox.sh/atproto-oauth` library is also client-side only **What Tangled has**: ```go // First-party OAuth (client-side): oauth.SaveSession() // For their web UI oauth.GetSession() // For their web UI oauth.AuthorizedClient() // Making requests TO PDS // Service-to-service: ServiceAuth.VerifyServiceAuth() // Same as indigo ``` **What Tangled does NOT have**: - Third-party OAuth token validation - DPoP proof verification for user tokens - PDS public key fetching/caching ### Option 4: Trust X-User-DID Header ❌ **Current implementation** - fundamentally insecure **Why it doesn't work**: - Anyone can set HTTP headers - No cryptographic verification - Trivial to impersonate any user - Violates basic security principles **Attack example**: ```bash # Attacker creates community as victim curl -X POST https://coves.social/xrpc/social.coves.community.create \ -H "X-User-DID: did:plc:victim123" \ -d '{"name": "impersonated-community", ...}' ``` ### Option 5: Proxy All Requests Through PDS ❌ **Idea**: Only accept requests from PDSes, not clients **Why it doesn't work**: - Breaks standard atProto architecture - Forces PDS to implement Coves-specific logic - Prevents third-party app development - Centralization defeats purpose of federation - No other AppView works this way ### Option 6: Require Users to Register API Keys ❌ **Idea**: Issue our own API keys to users **Why it doesn't work**: - Defeats purpose of decentralized identity (DID) - Users already have cryptographic identity via PDS - Creates vendor lock-in (keys only work with Coves) - Incompatible with atProto federation model - No other AppView requires this ## Implementation Approach ### Phased Rollout Strategy We'll implement OAuth validation in three phases to balance security, complexity, and time-to-alpha. #### Phase 1: Alpha - Basic JWT Validation (MVP) **Goal**: Unblock alpha launch with basic security **Implementation**: ```go func (m *AuthMiddleware) RequireAtProtoAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 1. Extract Authorization header authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "DPoP ") { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } token := strings.TrimPrefix(authHeader, "DPoP ") // 2. Parse JWT (unverified) claims, err := parseJWTClaims(token) if err != nil { http.Error(w, "Invalid token", http.StatusUnauthorized) return } // 3. Basic validation if time.Now().Unix() > claims.Expiry { http.Error(w, "Token expired", http.StatusUnauthorized) return } if !strings.Contains(claims.Scope, "atproto") { http.Error(w, "Invalid scope", http.StatusUnauthorized) return } // 4. Inject DID into context ctx := context.WithValue(r.Context(), UserDIDKey, claims.Subject) next.ServeHTTP(w, r.WithContext(ctx)) }) } ``` **What Phase 1 Validates**: - ✅ Token is a valid JWT structure - ✅ Token hasn't expired - ✅ Token has `atproto` scope - ✅ DID is extracted from `sub` claim **What Phase 1 Does NOT Validate**: - ❌ JWT signature (anyone can mint valid-looking JWTs) - ❌ Token was actually issued by claimed PDS - ❌ DPoP proof **Security Posture**: - Better than `X-User-DID` header (requires valid JWT structure) - Not production-ready (no signature verification) - Acceptable for alpha with trusted early users **Documentation Requirements**: ```go // TODO(OAuth-Phase2): Add JWT signature verification before beta // // Current implementation parses JWT claims but does not verify signatures. // This means tokens are not cryptographically validated against the PDS. // // Alpha security rationale: // - Better than X-User-DID (requires JWT structure, expiry) // - Acceptable risk for trusted early users // - Must be replaced before public beta // // See docs/PRD_OAUTH.md for Phase 2 implementation plan. ``` #### Phase 2: Beta - JWT Signature Verification **Goal**: Cryptographically verify tokens **Implementation**: ```go type TokenValidator struct { keyCache *PDSKeyCache // Caches PDS public keys idResolver *identity.Resolver } func (v *TokenValidator) ValidateAccessToken(ctx context.Context, token string) (*Claims, error) { // 1. Parse JWT with claims jwt, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { // 2. Extract issuer (PDS URL) claims := token.Claims.(jwt.MapClaims) issuer := claims["iss"].(string) // 3. Fetch PDS public keys (cached) keys, err := v.keyCache.GetKeys(ctx, issuer) if err != nil { return nil, err } // 4. Find matching key by kid kid := token.Header["kid"].(string) return keys.FindKey(kid) }) if err != nil { return nil, fmt.Errorf("invalid signature: %w", err) } // 5. Validate claims claims := jwt.Claims.(Claims) if !claims.HasScope("atproto") { return nil, errors.New("missing atproto scope") } return &claims, nil } type PDSKeyCache struct { cache *ttlcache.Cache } func (c *PDSKeyCache) GetKeys(ctx context.Context, pdsURL string) (*jwk.Set, error) { // Check cache if keys, ok := c.cache.Get(pdsURL); ok { return keys.(*jwk.Set), nil } // Fetch metadata metadata, err := fetchAuthServerMetadata(ctx, pdsURL) if err != nil { return nil, err } // Fetch JWKS keys, err := fetchJWKS(ctx, metadata.JWKSURI) if err != nil { return nil, err } // Cache with TTL (1 hour) c.cache.Set(pdsURL, keys, time.Hour) return keys, nil } ``` **What Phase 2 Adds**: - ✅ JWT signature verification - ✅ PDS public key fetching - ✅ Key caching (performance) - ✅ Cryptographic proof of token authenticity **Security Posture**: - Production-grade token validation - Cryptographically verifies token issued by claimed PDS - Acceptable for public beta #### Phase 3: Production - Full DPoP Validation **Goal**: Complete OAuth security compliance **Implementation**: ```go func (v *TokenValidator) ValidateDPoPBoundToken(ctx context.Context, r *http.Request) (*Claims, error) { // 1. Extract tokens accessToken := extractAccessToken(r) dpopProof := r.Header.Get("DPoP") // 2. Validate access token (Phase 2 logic) claims, err := v.ValidateAccessToken(ctx, accessToken) if err != nil { return nil, err } // 3. Parse DPoP proof dpop, err := jwt.Parse(dpopProof, func(token *jwt.Token) (interface{}, error) { // Public key is in the JWT itself (jwk claim) jwkClaim := token.Header["jwk"] return parseJWK(jwkClaim) }) if err != nil { return nil, fmt.Errorf("invalid DPoP proof: %w", err) } // 4. Validate DPoP proof dpopClaims := dpop.Claims.(DPoPClaims) // Check HTTP method matches if dpopClaims.HTM != r.Method { return nil, errors.New("DPoP htm mismatch") } // Check URL matches (without query params) expectedHTU := fmt.Sprintf("%s://%s%s", r.URL.Scheme, r.URL.Host, r.URL.Path) if dpopClaims.HTU != expectedHTU { return nil, errors.New("DPoP htu mismatch") } // Check access token hash tokenHash := sha256Hash(accessToken) if dpopClaims.ATH != tokenHash { return nil, errors.New("DPoP ath mismatch") } // 5. Verify DPoP key matches access token cnf dpopKeyThumbprint := computeJWKThumbprint(dpop.Header["jwk"]) if claims.Confirmation.JKT != dpopKeyThumbprint { return nil, errors.New("DPoP key binding mismatch") } // 6. Check and update nonce if err := v.validateAndRotateNonce(r, dpopClaims.Nonce); err != nil { // Return 401 with new nonce header return nil, &NonceError{NewNonce: generateNonce()} } return claims, nil } ``` **What Phase 3 Adds**: - ✅ DPoP proof verification - ✅ Token binding validation - ✅ Nonce handling (replay prevention) - ✅ Full OAuth/DPoP spec compliance **Security Posture**: - Full production security - Prevents token theft/replay attacks - Industry-standard OAuth 2.0 + DPoP ### Middleware Integration ```go // In cmd/server/main.go // Initialize auth middleware authMiddleware, err := middleware.NewAuthMiddleware(sessionStore, identityResolver) if err != nil { log.Fatal("Failed to initialize auth middleware:", err) } // Apply to community routes routes.RegisterCommunityRoutes(r, communityService, authMiddleware) ``` ```go // In internal/api/routes/community.go func RegisterCommunityRoutes(r chi.Router, service communities.Service, auth *middleware.AuthMiddleware) { // ... handlers initialization ... // Public endpoints (no auth) r.Get("/xrpc/social.coves.community.get", getHandler.HandleGet) r.Get("/xrpc/social.coves.community.list", listHandler.HandleList) r.Get("/xrpc/social.coves.community.search", searchHandler.HandleSearch) // Protected endpoints (require auth) r.Group(func(r chi.Router) { r.Use(auth.RequireAtProtoAuth) // Apply middleware r.Post("/xrpc/social.coves.community.create", createHandler.HandleCreate) r.Post("/xrpc/social.coves.community.update", updateHandler.HandleUpdate) r.Post("/xrpc/social.coves.community.subscribe", subscribeHandler.HandleSubscribe) r.Post("/xrpc/social.coves.community.unsubscribe", subscribeHandler.HandleUnsubscribe) }) } ``` ### Handler Updates Replace placeholder auth with context extraction: ```go // OLD (Phase 0 - Insecure) userDID := r.Header.Get("X-User-DID") // ❌ Anyone can forge // NEW (Phase 1+) userDID := middleware.GetUserDID(r) // ✅ From validated token if userDID == "" { // Should never happen (middleware validates) writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") return } ``` ## Implementation Checklist ### Phase 1 (Alpha) - ✅ COMPLETED (2025-10-16) - [x] Create `internal/api/middleware/auth.go` - [x] `RequireAuth` middleware - [x] `OptionalAuth` middleware - [x] `GetUserDID(r)` helper - [x] `GetJWTClaims(r)` helper - [x] Basic JWT parsing (no signature verification) - [x] Expiry validation - [x] Scope validation (lenient: allows empty, rejects wrong scopes) - [x] Issuer HTTPS validation - [x] DID format validation - [x] Security logging (IP, method, path, issuer, error type) - [x] Update community handlers to use `GetUserDID(r)` - [x] `create.go` (with CreatedByDID validation) - [x] `update.go` - [x] `subscribe.go` - [x] Update route registration in `routes/community.go` - [x] Add comprehensive middleware tests (`auth_test.go`) - [x] Valid token acceptance - [x] Missing/invalid header rejection - [x] Malformed token rejection - [x] Expired token rejection - [x] Missing DID rejection - [x] Optional auth scenarios - [x] Context helper functions - [x] Update E2E tests to use Bearer tokens - [x] Created `createTestJWT()` helper in `user_test.go` - [x] Updated `community_e2e_test.go` to use JWT auth - [x] Delete orphaned OAuth files - [x] Removed `dpop_transport.go` (referenced deleted packages) - [x] Removed `oauth_test.go` (tested deleted first-party OAuth) - [x] Documentation complete (README.md in internal/atproto/auth/) ### Phase 2 (Beta) - ✅ COMPLETED (2025-10-16) - [x] Implement JWT signature verification (`VerifyJWT` in `jwt.go`) - [x] Implement `CachedJWKSFetcher` with TTL (1 hour default) - [x] Add PDS metadata fetching - [x] `/.well-known/oauth-authorization-server` - [x] JWKS fetching from `jwks_uri` - [x] Add key caching layer (in-memory with TTL) - [x] Add ECDSA (ES256) support for atProto tokens - [x] Support for P-256, P-384, P-521 curves - [x] `toECPublicKey()` method in JWK - [x] Updated `JWKSFetcher` interface to return `interface{}` - [x] Add comprehensive error handling - [x] Add detailed security logging for validation failures - [x] JWT tests passing (`jwt_test.go`) - [x] Middleware tests passing (11/11 tests) - [x] Build verification successful - [ ] Integration tests with real PDS (deferred - requires live PDS) - [ ] Security audit (recommended before production) ### Phase 3 (Production) - Future Work **Status**: Not started (deferred to post-alpha) **Rationale**: Phase 2 provides production-grade JWT signature verification. DPoP adds defense-in-depth against token theft but is not critical for alpha/beta with proper HTTPS. - [ ] Implement DPoP proof parsing - [ ] Add DPoP validation logic - [ ] `htm` validation - [ ] `htu` validation - [ ] `ath` validation - [ ] `cnf`/`jkt` binding validation - [ ] Implement nonce management - [ ] Nonce generation - [ ] Nonce storage (per-user, per-server) - [ ] Nonce rotation - [ ] Add replay attack prevention - [ ] Add comprehensive JWKS fetcher tests ⚠️ HIGH PRIORITY - [ ] Cache hit/miss scenarios - [ ] Cache expiration behavior - [ ] JWKS endpoint failures - [ ] Malformed JWKS responses - [ ] Key rotation (kid mismatch) - [ ] Concurrent fetch handling (thundering herd - known limitation) - [ ] Add optional audience (`aud`) claim validation - [ ] Configurable expected audience from `APPVIEW_PUBLIC_URL` - [ ] Lenient mode (allow missing audience) - [ ] Strict mode (reject if audience doesn't match) - [ ] Fix thundering herd issue in JWKS cache - [ ] Implement singleflight pattern (`golang.org/x/sync/singleflight`) - [ ] Add tests for concurrent cache misses - [ ] Performance optimization - [ ] Profile JWKS fetch performance - [ ] Consider Redis for JWKS cache in multi-instance deployments - [ ] Complete security audit - [ ] Load testing ## Success Metrics ### Phase 1 (Alpha) - ✅ ACHIEVED - [x] All community endpoints reject requests without valid JWT structure - [x] Integration tests pass with mock tokens (11/11 middleware tests passing) - [x] Zero security regressions from X-User-DID (JWT validation is strictly better) - [x] E2E tests updated to use proper Bearer token authentication - [x] Build succeeds without compilation errors ### Phase 2 (Beta) - ✅ READY FOR TESTING - [x] 100% of tokens cryptographically verified (when AUTH_SKIP_VERIFY=false) - [x] ECDSA (ES256) token support for atProto ecosystem - [ ] PDS key cache hit rate >90% (requires production metrics) - [ ] Token validation <50ms p99 latency (requires production benchmarking) - [ ] Zero successful token forgery attempts in testing (ready for security audit) ### Phase 3 (Production) - [ ] Full DPoP spec compliance - [ ] Zero replay attacks in production - [ ] Token validation <100ms p99 latency - [ ] Security audit passed ## Security Considerations ### Phase 1 Limitations (MUST DOCUMENT) **Warning**: Phase 1 implementation does NOT verify JWT signatures. This means: - ❌ Anyone with JWT knowledge can mint "valid" tokens - ❌ No cryptographic proof of PDS issuance - ❌ Not suitable for untrusted users **Acceptable because**: - ✅ Alpha users are trusted early adopters - ✅ Better than X-User-DID header - ✅ Clear upgrade path to Phase 2 **Mitigation**: - Document limitations in README - Add warning to API documentation - Include TODO comments in code - Set clear deadline for Phase 2 (before public beta) ### Phase 2+ Security Once signature verification is implemented: - ✅ Cryptographic proof of token authenticity - ✅ Cannot forge tokens without PDS private key - ✅ Production-grade security ### Additional Hardening - **Rate limiting**: Prevent brute force token guessing - **Token revocation**: Check against revocation list (future) - **Audit logging**: Log all authentication attempts - **Monitoring**: Alert on validation failure spikes ## Open Questions 1. **PDS key caching**: What TTL is appropriate? - Proposal: 1 hour (balance freshness vs performance) - Allow PDS to hint with `Cache-Control` headers 2. **Nonce storage**: Where to store DPoP nonces? - Phase 1: Not needed - Phase 3: Redis or in-memory with TTL 3. **Error messages**: How detailed should auth errors be? - Proposal: Generic "Unauthorized" to prevent enumeration - Log detailed errors server-side for debugging 4. **Token audience**: Should we validate `aud` claim? - Proposal: Optional validation, log if present but mismatched - Some PDSes may not include `aud` 5. **Backward compatibility**: Support legacy auth during transition? - Proposal: No. Clean break at alpha launch - X-User-DID was never documented/public ## References - [atProto OAuth Spec](https://atproto.com/specs/oauth) - [RFC 9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://datatracker.ietf.org/doc/html/rfc9449) - [RFC 7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) - [Indigo OAuth Client Implementation](https://pkg.go.dev/github.com/bluesky-social/indigo/atproto/auth/oauth) - Tangled codebase analysis: `/home/bretton/Code/tangled/core` ## Appendix: Token Examples ### Valid Access Token (Decoded) **Header**: ```json { "alg": "ES256", "typ": "at+jwt", "kid": "did:plc:alice#atproto-pds" } ``` **Payload**: ```json { "iss": "https://pds.alice.com", "sub": "did:plc:alice123", "aud": "https://coves.social", "scope": "atproto", "exp": 1698765432, "iat": 1698761832, "jti": "token-unique-id-123", "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" } } ``` ### Valid DPoP Proof (Decoded) **Header**: ```json { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs", "y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA" } } ``` **Payload**: ```json { "jti": "proof-unique-id-456", "htm": "POST", "htu": "https://coves.social/xrpc/social.coves.community.create", "iat": 1698761832, "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo", "nonce": "server-nonce-abc123" } ``` ## Appendix: Comparison with Other Systems | Feature | Coves (Phase 1) | Coves (Phase 3) | Tangled | Bluesky AppView | |---------|-----------------|-----------------|---------|-----------------| | User OAuth Validation | Basic JWT parse | Full DPoP | ❌ None | ✅ Full | | Signature Verification | ❌ | ✅ | ❌ | ✅ | | DPoP Proof Validation | ❌ | ✅ | ❌ | ✅ | | Service Auth | ❌ | ❌ | ✅ | ✅ | | First-Party OAuth | ✅ | ✅ | ✅ | ✅ | | Third-Party Support | Partial | ✅ | ❌ | ✅ | **Key Takeaway**: Most atProto projects (including Tangled) focus on first-party OAuth only. Coves needs third-party validation because communities are inherently multi-user and social.