A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
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)