A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
73
fork

Configure Feed

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

at 97d1b3cdd50e4727e5db3c498f4e8bb73851fd39 184 lines 5.6 kB view raw
1package auth 2 3import ( 4 "atcr.io/pkg/atproto" 5 "bytes" 6 "context" 7 "crypto/sha256" 8 "encoding/hex" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "sync" 14 "time" 15 16 "github.com/bluesky-social/indigo/atproto/identity" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18) 19 20// CachedSession represents a cached session 21type CachedSession struct { 22 DID string 23 Handle string 24 PDS string 25 AccessToken string 26 ExpiresAt time.Time 27} 28 29// SessionValidator validates ATProto credentials 30type SessionValidator struct { 31 directory identity.Directory 32 httpClient *http.Client 33 cache map[string]*CachedSession 34 cacheMu sync.RWMutex 35} 36 37// NewSessionValidator creates a new ATProto session validator 38func NewSessionValidator() *SessionValidator { 39 return &SessionValidator{ 40 directory: identity.DefaultDirectory(), 41 httpClient: &http.Client{}, 42 cache: make(map[string]*CachedSession), 43 } 44} 45 46// getCacheKey generates a cache key from username and password 47func getCacheKey(username, password string) string { 48 h := sha256.New() 49 h.Write([]byte(username + ":" + password)) 50 return hex.EncodeToString(h.Sum(nil)) 51} 52 53// getCachedSession retrieves a cached session if valid 54func (v *SessionValidator) getCachedSession(cacheKey string) (*CachedSession, bool) { 55 v.cacheMu.RLock() 56 defer v.cacheMu.RUnlock() 57 58 session, ok := v.cache[cacheKey] 59 if !ok { 60 return nil, false 61 } 62 63 // Check if expired (with 5 minute buffer) 64 if time.Now().After(session.ExpiresAt.Add(-5 * time.Minute)) { 65 return nil, false 66 } 67 68 return session, true 69} 70 71// setCachedSession stores a session in the cache 72func (v *SessionValidator) setCachedSession(cacheKey string, session *CachedSession) { 73 v.cacheMu.Lock() 74 defer v.cacheMu.Unlock() 75 v.cache[cacheKey] = session 76} 77 78// SessionResponse represents the response from createSession 79type SessionResponse struct { 80 DID string `json:"did"` 81 Handle string `json:"handle"` 82 AccessJWT string `json:"accessJwt"` 83 RefreshJWT string `json:"refreshJwt"` 84 Email string `json:"email,omitempty"` 85 AccessToken string `json:"access_token,omitempty"` // Alternative field name 86} 87 88// CreateSessionAndGetToken creates a session and returns the DID, handle, and access token 89func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identifier, password string) (did, handle, accessToken string, err error) { 90 // Check cache first 91 cacheKey := getCacheKey(identifier, password) 92 if cached, ok := v.getCachedSession(cacheKey); ok { 93 fmt.Printf("DEBUG [atproto/session]: Using cached session for %s (DID=%s)\n", identifier, cached.DID) 94 return cached.DID, cached.Handle, cached.AccessToken, nil 95 } 96 97 fmt.Printf("DEBUG [atproto/session]: No cached session for %s, creating new session\n", identifier) 98 99 // Resolve identifier to PDS endpoint 100 atID, err := syntax.ParseAtIdentifier(identifier) 101 if err != nil { 102 return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) 103 } 104 105 ident, err := v.directory.Lookup(ctx, *atID) 106 if err != nil { 107 return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) 108 } 109 110 did = ident.DID.String() 111 pds := ident.PDSEndpoint() 112 if pds == "" { 113 return "", "", "", fmt.Errorf("no PDS endpoint found for %q", identifier) 114 } 115 116 // Create session 117 sessionResp, err := v.createSession(ctx, pds, identifier, password) 118 if err != nil { 119 return "", "", "", fmt.Errorf("authentication failed: %w", err) 120 } 121 122 // Cache the session (ATProto sessions typically last 2 hours) 123 v.setCachedSession(cacheKey, &CachedSession{ 124 DID: sessionResp.DID, 125 Handle: sessionResp.Handle, 126 PDS: pds, 127 AccessToken: sessionResp.AccessJWT, 128 ExpiresAt: time.Now().Add(2 * time.Hour), 129 }) 130 fmt.Printf("DEBUG [atproto/session]: Cached session for %s (expires in 2 hours)\n", identifier) 131 132 return sessionResp.DID, sessionResp.Handle, sessionResp.AccessJWT, nil 133} 134 135// createSession calls com.atproto.server.createSession 136func (v *SessionValidator) createSession(ctx context.Context, pdsEndpoint, identifier, password string) (*SessionResponse, error) { 137 payload := map[string]string{ 138 "identifier": identifier, 139 "password": password, 140 } 141 142 body, err := json.Marshal(payload) 143 if err != nil { 144 return nil, fmt.Errorf("failed to marshal request: %w", err) 145 } 146 147 url := fmt.Sprintf("%s%s", pdsEndpoint, atproto.ServerCreateSession) 148 fmt.Printf("DEBUG [atproto/session]: POST %s\n", url) 149 150 req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) 151 if err != nil { 152 return nil, err 153 } 154 155 req.Header.Set("Content-Type", "application/json") 156 157 resp, err := v.httpClient.Do(req) 158 if err != nil { 159 fmt.Printf("DEBUG [atproto/session]: HTTP request failed: %v\n", err) 160 return nil, fmt.Errorf("failed to create session: %w", err) 161 } 162 defer resp.Body.Close() 163 164 fmt.Printf("DEBUG [atproto/session]: Got HTTP status %d\n", resp.StatusCode) 165 166 if resp.StatusCode == http.StatusUnauthorized { 167 bodyBytes, _ := io.ReadAll(resp.Body) 168 fmt.Printf("DEBUG [atproto/session]: Unauthorized response: %s\n", string(bodyBytes)) 169 return nil, fmt.Errorf("invalid credentials") 170 } 171 172 if resp.StatusCode != http.StatusOK { 173 bodyBytes, _ := io.ReadAll(resp.Body) 174 fmt.Printf("DEBUG [atproto/session]: Error response: %s\n", string(bodyBytes)) 175 return nil, fmt.Errorf("create session failed with status %d: %s", resp.StatusCode, string(bodyBytes)) 176 } 177 178 var sessionResp SessionResponse 179 if err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil { 180 return nil, fmt.Errorf("failed to decode response: %w", err) 181 } 182 183 return &sessionResp, nil 184}