From 2b0501a4373929f4fbda193f3fecc9cade894321 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sat, 25 Oct 2025 00:55:22 -0500 Subject: [PATCH] more logging --- CLAUDE.md | 22 ++++++++++------------ README.md | 4 ++-- docs/APPVIEW-UI-V1.md | 4 ++-- docs/EMBEDDED_PDS.md | 12 +++++++----- pkg/hold/pds/auth.go | 5 +++++ pkg/hold/pds/xrpc.go | 15 +++++++++++++++ 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9274451..1618745 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -228,23 +228,21 @@ ATCR implements the full ATProto OAuth specification with mandatory security fea - `GetDefaultScopes()` - returns ATCR registry scopes - All OAuth flows (authorization, token exchange, refresh) in one place -2. **DPoP Transport** (`transport.go`) - HTTP RoundTripper that auto-adds DPoP headers - -3. **Token Storage** (`tokenstorage.go`) - Persists refresh tokens and DPoP keys for AppView +2. **Token Storage** (`store.go`) - Persists OAuth sessions for AppView - File-based storage in `/var/lib/atcr/refresh-tokens.json` (AppView) - Client uses `~/.atcr/oauth-token.json` (credential helper) -4. **Refresher** (`refresher.go`) - Token refresh manager for AppView - - Caches access tokens with automatic refresh +3. **Refresher** (`refresher.go`) - Token refresh manager for AppView + - Caches OAuth sessions with automatic token refresh (handled by indigo library) - Per-DID locking prevents concurrent refresh races - Uses Client methods for consistency -5. **Server** (`server.go`) - OAuth authorization endpoints for AppView +4. **Server** (`server.go`) - OAuth authorization endpoints for AppView - `GET /auth/oauth/authorize` - starts OAuth flow - `GET /auth/oauth/callback` - handles OAuth callback - Uses Client methods for authorization and token exchange -6. **Interactive Flow** (`flow.go`) - Reusable OAuth flow for CLI tools +5. **Interactive Flow** (`interactive.go`) - Reusable OAuth flow for CLI tools - Used by credential helper and hold service registration - Two-phase callback setup ensures PAR metadata availability @@ -259,7 +257,7 @@ ATCR implements the full ATProto OAuth specification with mandatory security fea - PAR request with DPoP header → get request_uri - User authorizes in browser - AppView exchanges code for OAuth token with DPoP proof - - AppView stores: OAuth token, refresh token, DPoP key, DID, handle + - AppView stores: OAuth session (tokens managed by indigo library with DPoP), DID, handle 5. AppView shows device approval page: "Can [device] push to your account?" 6. User approves device 7. AppView issues registry JWT with validated DID @@ -272,10 +270,10 @@ Later (subsequent docker push): 12. Helper returns cached registry JWT (or re-authenticates if expired) ``` -**Key distinction:** The credential helper never manages OAuth tokens or DPoP keys directly. AppView owns the OAuth session and issues registry JWTs to the credential helper. This means AppView has access to user OAuth tokens and DPoP keys, which it needs for: -- Writing manifests to user's PDS -- Validating user sessions -- Delegating access to hold services +**Key distinction:** The credential helper never manages OAuth tokens directly. AppView owns the OAuth session (including DPoP handling via indigo library) and issues registry JWTs to the credential helper. AppView needs the OAuth session for: +- Writing manifests to user's PDS (with DPoP authentication) +- Getting service tokens from user's PDS (with DPoP authentication) +- Service tokens are then used to authenticate to hold services (Bearer tokens, not DPoP) **Security:** - Tokens validated against authoritative source (user's PDS) diff --git a/README.md b/README.md index e3788b5..a2093b7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ atcr.io/did:plc:xyz123/myapp:latest - Users can deploy their own storage and control access via crew membership 3. **Credential Helper** - Client authentication - - ATProto OAuth with DPoP + - ATProto OAuth (DPoP handled transparently) - Automatic authentication on first push/pull **Storage model:** @@ -43,7 +43,7 @@ atcr.io/did:plc:xyz123/myapp:latest - ✅ **OCI-compliant** - Works with Docker, containerd, podman - ✅ **Decentralized** - You own your manifest data via your PDS -- ✅ **ATProto OAuth** - Secure authentication with DPoP +- ✅ **ATProto OAuth** - Secure authentication (DPoP-compliant) - ✅ **BYOS** - Deploy your own storage service - ✅ **Web UI** - Browse, search, star repositories - ✅ **Multi-backend** - S3, Storj, Minio, Azure, GCS, filesystem diff --git a/docs/APPVIEW-UI-V1.md b/docs/APPVIEW-UI-V1.md index debafb5..57705dc 100644 --- a/docs/APPVIEW-UI-V1.md +++ b/docs/APPVIEW-UI-V1.md @@ -16,7 +16,7 @@ The ATCR AppView UI provides a web interface for discovering, managing, and conf - **Frontend:** TBD (Go templates/Templ or separate SPA) - **Database:** SQLite (firehose data cache) - **Styling:** TBD (plain CSS, Tailwind, etc.) -- **Authentication:** OAuth with DPoP (reuse existing implementation) +- **Authentication:** ATProto OAuth (DPoP handled by indigo library) ### Components @@ -501,7 +501,7 @@ Reuse existing OAuth implementation from credential helper and AppView. 2. Redirects to `/auth/oauth/login?return_to=/ui/images` 3. User enters handle (e.g., "alice.bsky.social") 4. Server resolves handle → DID → PDS → OAuth server -5. Server initiates OAuth flow with PAR + DPoP +5. Server initiates ATProto OAuth flow with PAR (DPoP handled by indigo library) 6. User redirected to PDS for authorization 7. OAuth callback to `/auth/oauth/callback` 8. Server exchanges code for token, validates with PDS diff --git a/docs/EMBEDDED_PDS.md b/docs/EMBEDDED_PDS.md index 23471fe..f4901d7 100644 --- a/docs/EMBEDDED_PDS.md +++ b/docs/EMBEDDED_PDS.md @@ -250,9 +250,11 @@ Issue: IP address with port not supported in aud field ### Potential Solutions -#### Option A: Direct User-to-Hold Authentication +#### Option A: Direct User-to-Hold Authentication (NOT IMPLEMENTED) -Users authenticate directly to holds (bypassing AppView service tokens). +**Note:** This option was considered but NOT implemented. ATCR uses service tokens exclusively for AppView→Hold authentication. + +Users would authenticate directly to holds (bypassing AppView service tokens). **Pros:** - ✅ Clear trust model (user ↔ hold) @@ -315,9 +317,9 @@ Use service tokens when available, fall back to API keys for BYOS holds. 2. Clear security model for hold operators **Long-term:** -1. Explore direct user-to-hold OAuth -2. Credential helper manages multiple hold sessions -3. Auto-discover and authenticate to new holds +1. Continue using service tokens (current implementation) +2. Explore optimizations for service token caching +3. Document security model more clearly ### Understanding getServiceAuth diff --git a/pkg/hold/pds/auth.go b/pkg/hold/pds/auth.go index 3dee9cf..6cfcac8 100644 --- a/pkg/hold/pds/auth.go +++ b/pkg/hold/pds/auth.go @@ -10,6 +10,7 @@ import ( "slices" "strings" "time" + "log" "atcr.io/pkg/atproto" "github.com/bluesky-social/indigo/atproto/atcrypto" @@ -425,6 +426,8 @@ func ValidateServiceToken(r *http.Request, holdDID string, httpClient HTTPClient return nil, fmt.Errorf("missing token") } + log.Printf("[ValidateServiceToken] Validating service token for hold %s", holdDID) + // Manually parse JWT (bypass golang-jwt since it doesn't support ES256K algorithm used by ATProto) // Split token: header.payload.signature tokenParts := strings.Split(tokenString, ".") @@ -490,6 +493,8 @@ func ValidateServiceToken(r *http.Request, holdDID string, httpClient HTTPClient return nil, fmt.Errorf("signature verification failed: %w", err) } + log.Printf("[ValidateServiceToken] Successfully validated service token for user %s", issuerDID) + // Return validated user return &ValidatedUser{ DID: issuerDID, diff --git a/pkg/hold/pds/xrpc.go b/pkg/hold/pds/xrpc.go index 42d2a67..139dfcb 100644 --- a/pkg/hold/pds/xrpc.go +++ b/pkg/hold/pds/xrpc.go @@ -1128,6 +1128,8 @@ func (h *XRPCHandler) HandleAtprotoDID(w http.ResponseWriter, r *http.Request) { // This endpoint allows authenticated users to request crew membership // Authorization is checked against captain record settings func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request) { + log.Printf("[HandleRequestCrew] Starting crew membership request") + // Get authenticated user from context (if coming through middleware) // Otherwise validate directly (for tests or direct handler calls) user := getUserFromContext(r) @@ -1135,10 +1137,12 @@ func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request) var err error user, err = ValidateDPoPRequest(r, h.httpClient) if err != nil { + log.Printf("[HandleRequestCrew] Authentication failed: %v", err) http.Error(w, fmt.Sprintf("authentication failed: %v", err), http.StatusUnauthorized) return } } + log.Printf("[HandleRequestCrew] Authenticated user: %s", user.DID) // Parse request body (optional parameters) var req struct { @@ -1149,17 +1153,21 @@ func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request) // Body is optional - if empty, just use defaults if r.Body != nil && r.ContentLength > 0 { if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Printf("[HandleRequestCrew] Failed to parse request body: %v", err) http.Error(w, fmt.Sprintf("invalid request body: %v", err), http.StatusBadRequest) return } } // Get captain record to check authorization settings + log.Printf("[HandleRequestCrew] Getting captain record...") _, captain, err := h.pds.GetCaptainRecord(r.Context()) if err != nil { + log.Printf("[HandleRequestCrew] Failed to get captain record: %v", err) http.Error(w, fmt.Sprintf("failed to get captain record: %v", err), http.StatusInternalServerError) return } + log.Printf("[HandleRequestCrew] Captain record retrieved: owner=%s, allowAllCrew=%v", captain.Owner, captain.AllowAllCrew) // Check authorization: // 1. If allowAllCrew is true, any authenticated user can join @@ -1181,15 +1189,19 @@ func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request) // Check if user is already a crew member // List all crew members and check if this DID is already present + log.Printf("[HandleRequestCrew] Checking existing crew membership...") crew, err := h.pds.ListCrewMembers(r.Context()) if err != nil { + log.Printf("[HandleRequestCrew] Failed to list crew members: %v", err) http.Error(w, fmt.Sprintf("failed to list crew members: %v", err), http.StatusInternalServerError) return } + log.Printf("[HandleRequestCrew] Found %d existing crew members", len(crew)) for _, member := range crew { if member.Record.Member == user.DID { // Already a crew member, return success with existing record + log.Printf("[HandleRequestCrew] User is already a crew member (rkey=%s)", member.Rkey) response := map[string]any{ "uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), atproto.CrewCollection, member.Rkey), "cid": member.Cid.String(), @@ -1204,11 +1216,14 @@ func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request) } // Create new crew record + log.Printf("[HandleRequestCrew] Creating new crew record for user %s (role=%s, permissions=%v)", user.DID, req.Role, req.Permissions) recordCID, err := h.pds.AddCrewMember(r.Context(), user.DID, req.Role, req.Permissions) if err != nil { + log.Printf("[HandleRequestCrew] Failed to create crew record: %v", err) http.Error(w, fmt.Sprintf("failed to create crew record: %v", err), http.StatusInternalServerError) return } + log.Printf("[HandleRequestCrew] Successfully created crew record (CID=%s)", recordCID.String()) // Return success response // Note: rkey is generated by AddCrewMember (TID), we don't have direct access to it