A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix security issue with refresh tokens. on logout force a full re-auth

+10 -111
+1 -5
cmd/appview/serve.go
··· 469 469 Templates: templates, 470 470 }).Methods("GET") 471 471 472 - router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{ 473 - Refresher: refresher, 474 - Directory: oauthApp.Directory(), 475 - SessionStore: sessionStore, 476 - }).Methods("POST") 472 + router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{}).Methods("POST") 477 473 478 474 // Public routes (with optional auth for navbar) 479 475 // SECURITY: Public pages use read-only DB
-95
pkg/appview/handlers/auth.go
··· 1 1 package handlers 2 2 3 3 import ( 4 - "fmt" 5 4 "html/template" 6 5 "net/http" 7 - "time" 8 - 9 - "atcr.io/pkg/auth/oauth" 10 - "github.com/bluesky-social/indigo/atproto/identity" 11 - "github.com/bluesky-social/indigo/atproto/syntax" 12 6 ) 13 7 14 8 // LoginHandler shows the OAuth login form ··· 38 32 39 33 // LoginSubmitHandler processes the login form submission 40 34 type LoginSubmitHandler struct { 41 - Refresher *oauth.Refresher 42 - Directory identity.Directory 43 - SessionStore UISessionStore 44 - } 45 - 46 - // UISessionStore is the interface for UI session management 47 - type UISessionStore interface { 48 - CreateWithOAuth(did, handle, pdsEndpoint, oauthSessionID string, duration time.Duration) (string, error) 49 35 } 50 36 51 37 func (h *LoginSubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 65 51 return 66 52 } 67 53 68 - // Attempt silent login first 69 - if h.Refresher != nil && h.Directory != nil && h.SessionStore != nil { 70 - // Parse handle 71 - handleSyntax, err := syntax.ParseHandle(handle) 72 - if err == nil { 73 - // Resolve handle to identity (DID + PDS endpoint) 74 - ident, err := h.Directory.LookupHandle(r.Context(), handleSyntax) 75 - if err == nil { 76 - did := ident.DID.String() 77 - 78 - // Try to get existing OAuth session 79 - session, err := h.Refresher.GetSession(r.Context(), did) 80 - if err == nil { 81 - // Check if the session has all required scopes 82 - requiredScopes := oauth.GetDefaultScopes() 83 - sessionScopes := session.Data.Scopes 84 - 85 - if !hasAllScopes(sessionScopes, requiredScopes) { 86 - fmt.Printf("DEBUG [auth]: Session scopes mismatch for %s. Required: %v, Have: %v. Forcing re-auth.\n", 87 - handle, requiredScopes, sessionScopes) 88 - } else { 89 - // Found valid OAuth session with all required scopes! Create UI session silently 90 - fmt.Printf("DEBUG [auth]: Silent login successful for %s (DID: %s)\n", handle, did) 91 - 92 - // Get PDS endpoint from identity 93 - pdsEndpoint := ident.PDSEndpoint() 94 - 95 - // Get OAuth sessionID from refresher 96 - sessionID := h.Refresher.GetSessionID(did) 97 - 98 - uiSessionID, err := h.SessionStore.CreateWithOAuth(did, handle, pdsEndpoint, sessionID, 30*24*time.Hour) 99 - if err == nil { 100 - // Set session cookie 101 - http.SetCookie(w, &http.Cookie{ 102 - Name: "atcr_session", 103 - Value: uiSessionID, 104 - Path: "/", 105 - MaxAge: 30 * 86400, // 30 days 106 - HttpOnly: true, 107 - Secure: true, 108 - SameSite: http.SameSiteLaxMode, 109 - }) 110 - 111 - // Redirect to return URL 112 - fmt.Printf("DEBUG [auth]: Silent login complete, redirecting to %s\n", returnTo) 113 - http.Redirect(w, r, returnTo, http.StatusFound) 114 - return 115 - } 116 - 117 - fmt.Printf("WARNING [auth]: Failed to create UI session during silent login: %v\n", err) 118 - } 119 - } else { 120 - fmt.Printf("DEBUG [auth]: No valid OAuth session found for %s: %v\n", handle, err) 121 - } 122 - } else { 123 - fmt.Printf("DEBUG [auth]: Failed to resolve handle %s: %v\n", handle, err) 124 - } 125 - } else { 126 - fmt.Printf("DEBUG [auth]: Failed to parse handle %s: %v\n", handle, err) 127 - } 128 - } 129 - 130 - // Silent login failed or not configured - proceed with full OAuth flow 131 - fmt.Printf("DEBUG [auth]: Proceeding with full OAuth flow for %s\n", handle) 132 - 133 54 // Store return_to in cookie so callback can use it 134 55 http.SetCookie(w, &http.Cookie{ 135 56 Name: "oauth_return_to", ··· 144 65 // Redirect to OAuth authorize with handle 145 66 http.Redirect(w, r, "/auth/oauth/authorize?handle="+handle, http.StatusFound) 146 67 } 147 - 148 - // hasAllScopes checks if grantedScopes contains all requiredScopes 149 - func hasAllScopes(grantedScopes, requiredScopes []string) bool { 150 - grantedSet := make(map[string]bool) 151 - for _, scope := range grantedScopes { 152 - grantedSet[scope] = true 153 - } 154 - 155 - for _, required := range requiredScopes { 156 - if !grantedSet[required] { 157 - return false 158 - } 159 - } 160 - 161 - return true 162 - }
+4 -9
pkg/atproto/profile.go
··· 11 11 12 12 // EnsureProfile checks if a user's profile exists and creates it if needed 13 13 // This should be called during authentication (OAuth exchange or token service) 14 - // If defaultHoldEndpoint is provided and profile doesn't exist, creates profile with that default 14 + // If defaultHoldEndpoint is provided, creates profile with that default (or empty if not provided) 15 15 func EnsureProfile(ctx context.Context, client *Client, defaultHoldEndpoint string) error { 16 16 // Check if profile already exists 17 17 profile, err := client.GetRecord(ctx, SailorProfileCollection, ProfileRKey) ··· 20 20 return nil 21 21 } 22 22 23 - // Profile doesn't exist 24 - // Only create if we have a default hold endpoint to set 25 - if defaultHoldEndpoint == "" { 26 - // No default configured, don't create empty profile 27 - return nil 28 - } 29 - 30 - // Create new profile with default hold 23 + // Profile doesn't exist - create it 24 + // defaultHoldEndpoint can be empty string (user will need to configure it later) 31 25 newProfile := NewSailorProfileRecord(defaultHoldEndpoint) 32 26 33 27 _, err = client.PutRecord(ctx, SailorProfileCollection, ProfileRKey, newProfile) ··· 35 29 return fmt.Errorf("failed to create sailor profile: %w", err) 36 30 } 37 31 32 + fmt.Printf("DEBUG [profile]: Created sailor profile with defaultHold=%s\n", defaultHoldEndpoint) 38 33 return nil 39 34 } 40 35
+5 -2
pkg/auth/oauth/server.go
··· 267 267 // Create authenticated atproto client using the indigo session's API client 268 268 client := atproto.NewClientWithIndigoClient(pdsEndpoint, did, session.APIClient()) 269 269 270 - // Ensure sailor profile exists (creates with default hold on first login) 270 + // Ensure sailor profile exists (creates with default hold if configured, or empty profile if not) 271 + fmt.Printf("DEBUG [oauth/server]: Ensuring profile exists for %s (defaultHold=%s)\n", did, s.defaultHoldEndpoint) 271 272 if err := atproto.EnsureProfile(ctx, client, s.defaultHoldEndpoint); err != nil { 272 273 fmt.Printf("WARNING [oauth/server]: Failed to ensure profile for %s: %v\n", did, err) 273 - // Continue anyway - profile creation is not critical 274 + // Continue anyway - profile creation is not critical for avatar fetch 275 + } else { 276 + fmt.Printf("DEBUG [oauth/server]: Profile ensured for %s\n", did) 274 277 } 275 278 276 279 // Fetch user's profile record from PDS (contains blob references)