···
+
# 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
+
- ✅ 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).
+
- 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.
+
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
+
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
+
- Malicious updates to communities
+
Example of current vulnerability:
+
# 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.
+
### Existing Infrastructure
+
#### 1. First-Party OAuth (Client-Side)
+
- **Location**: `internal/core/oauth/`, `internal/api/handlers/oauth/`
+
- **Purpose**: For a potential Coves web frontend
+
- Login flows (`/oauth/login`, `/oauth/callback`)
+
- Session management (cookie + database)
+
- 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**:
+
// INSECURE - allows impersonation
+
userDID := r.Header.Get("X-User-DID")
+
- `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:
+
┌─────────────┐ ┌─────────────┐
+
│ │ 1. Initiate OAuth flow │ │
+
│ │──────────────────────────────>│ │
+
│ │ 2. User authorizes │ │
+
│ │ 3. Receive access token │ │
+
│ │<──────────────────────────────│ │
+
└─────────────┘ └─────────────┘
+
│ 4. Make authenticated request
+
│ Authorization: DPoP <token>
+
│ AppView │ 5. Validate token & DPoP
+
│ │ 6. Extract user DID
+
Third-party clients send two headers:
+
**1. Authorization Header**
+
Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImRpZDpwbGM6YWxpY2UjYXRwcm90by1wZHMifQ...
+
Format: `DPoP <access_token>`
+
The access token is a JWT containing:
+
"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"
+
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0...
+
The DPoP proof is a JWT proving possession of the private key bound to the access token:
+
"jwk": { // Public key (ephemeral)
+
"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 <token>` header
+
- Extract `DPoP: <proof>` header
+
#### 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
+
- 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
+
- ClientApp.StartAuthFlow() // Initiate login
+
- ClientApp.ProcessCallback() // Handle OAuth callback
+
- ClientSession.RefreshToken() // Refresh tokens
+
**What it does NOT provide**:
+
// 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)
+
### 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
+
// First-party OAuth (client-side):
+
oauth.SaveSession() // For their web UI
+
oauth.GetSession() // For their web UI
+
oauth.AuthorizedClient() // Making requests TO PDS
+
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
+
# 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
+
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)
+
token := strings.TrimPrefix(authHeader, "DPoP ")
+
// 2. Parse JWT (unverified)
+
claims, err := parseJWTClaims(token)
+
http.Error(w, "Invalid token", http.StatusUnauthorized)
+
if time.Now().Unix() > claims.Expiry {
+
http.Error(w, "Token expired", http.StatusUnauthorized)
+
if !strings.Contains(claims.Scope, "atproto") {
+
http.Error(w, "Invalid scope", http.StatusUnauthorized)
+
// 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
+
- 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**:
+
// 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
+
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)
+
// 4. Find matching key by kid
+
kid := token.Header["kid"].(string)
+
return keys.FindKey(kid)
+
return nil, fmt.Errorf("invalid signature: %w", err)
+
claims := jwt.Claims.(Claims)
+
if !claims.HasScope("atproto") {
+
return nil, errors.New("missing atproto scope")
+
type PDSKeyCache struct {
+
func (c *PDSKeyCache) GetKeys(ctx context.Context, pdsURL string) (*jwk.Set, error) {
+
if keys, ok := c.cache.Get(pdsURL); ok {
+
return keys.(*jwk.Set), nil
+
metadata, err := fetchAuthServerMetadata(ctx, pdsURL)
+
keys, err := fetchJWKS(ctx, metadata.JWKSURI)
+
// Cache with TTL (1 hour)
+
c.cache.Set(pdsURL, keys, time.Hour)
+
- ✅ JWT signature verification
+
- ✅ PDS public key fetching
+
- ✅ Key caching (performance)
+
- ✅ Cryptographic proof of token authenticity
+
- 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
+
func (v *TokenValidator) ValidateDPoPBoundToken(ctx context.Context, r *http.Request) (*Claims, error) {
+
accessToken := extractAccessToken(r)
+
dpopProof := r.Header.Get("DPoP")
+
// 2. Validate access token (Phase 2 logic)
+
claims, err := v.ValidateAccessToken(ctx, accessToken)
+
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)
+
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()}
+
- ✅ DPoP proof verification
+
- ✅ Token binding validation
+
- ✅ Nonce handling (replay prevention)
+
- ✅ Full OAuth/DPoP spec compliance
+
- Full production security
+
- Prevents token theft/replay attacks
+
- Industry-standard OAuth 2.0 + DPoP
+
### Middleware Integration
+
// In cmd/server/main.go
+
// Initialize auth middleware
+
authMiddleware, err := middleware.NewAuthMiddleware(sessionStore, identityResolver)
+
log.Fatal("Failed to initialize auth middleware:", err)
+
// Apply to community routes
+
routes.RegisterCommunityRoutes(r, communityService, authMiddleware)
+
// 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)
+
Replace placeholder auth with context extraction:
+
// OLD (Phase 0 - Insecure)
+
userDID := r.Header.Get("X-User-DID") // ❌ Anyone can forge
+
userDID := middleware.GetUserDID(r) // ✅ From validated token
+
// Should never happen (middleware validates)
+
writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required")
+
## 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 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
+
- [ ] `cnf`/`jkt` binding validation
+
- [ ] Implement nonce management
+
- [ ] Nonce storage (per-user, per-server)
+
- [ ] 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
+
### 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
+
- Document limitations in README
+
- Add warning to API documentation
+
- Include TODO comments in code
+
- Set clear deadline for Phase 2 (before public beta)
+
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
+
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 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
+
- [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)
+
"kid": "did:plc:alice#atproto-pds"
+
"iss": "https://pds.alice.com",
+
"sub": "did:plc:alice123",
+
"aud": "https://coves.social",
+
"jti": "token-unique-id-123",
+
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
+
### Valid DPoP Proof (Decoded)
+
"x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
+
"y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA"
+
"jti": "proof-unique-id-456",
+
"htu": "https://coves.social/xrpc/social.coves.community.create",
+
"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.